前端工程

webpack介绍

webpack概念

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),将项目当作一个整体,通过一个给定的的主文件,webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包成一个或多个浏览器可识别的js文件

组成:

  • 入口(entry)
    入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始
  • 输出(output)
    output 属性告诉 webpack 在哪里输出它所创建的 bundles ,以及如何命名这些文件,默认值为 ./dist
  • loader
    loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)
  • 插件(plugins)
    loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量
  • 模式
    通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化
  • devtool
    用于控制是否以及如何生成源代码映射,可以帮助开发快速定位错误

webpack构建流程

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

loader和plugin

常见loader

  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  • slint-loader:通过 SLint 检查 JavaScript 代码
  • babel-loader:把 ES6 转换成 ES5
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去

常见plugin

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • open-browser-webpack-plugin: 自动打开浏览器
  • clean-webpack-plugin: 清除文件
  • html-webpack-plugin: 向文件中插入html

loader和plugin的区别

loader 加载器

loader是指webpack打包方案,对于很多文件例如less,icon,图片等webpack不知道如何打包,通过loader来告诉webapck如何打包,让 webpack 拥有了加载和解析非 JavaScript 文件的能力
在 module.rules 中配置,也就是说他作为模块的解析规则而存在,类型为数组

Plugin 插件

plugin是指插件包,plugins里面的插件会帮助我们做一些其他的事情, 让 webpack 具有更多的灵活性,提升开发效率。
在 plugins 中单独配置。类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入
对于loader,它就是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程
plugin是一个扩展器,它丰富了wepack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

bundle,chunk,module


bundle 是由 webpack 打包出来的文件,chunk 是指 webpack 在进行模块的依赖分析的时候,代码分割出来的代码块。module是开发中的单个模块

webpack配置跨域

    devServer: {
        contentBase: './dist',  // 起一个在dist文件夹下的服务器
        open: true,    // 自动打开浏览器并访问服务器地址
        proxy: {   // 跨域代理
            '/api': 'http: //localhost:3000'  // 如果使用/api,会被转发(代理)到该地址
        },
        port: 8080,
        hot: true,  // 开启HMR功能
        hotOnly: true  // 即使HMR不生效,也不自动刷新
    },
1
2
3
4
5
6
7
8
9
10

webpack模块热更新

HMR的有两种实现方式,一种是通过插件HotModuleReplacementPlugin和devserver配和实现,一种是通过在自定义开发服务下,使用插件webpack-dev-middleware和webpack-Hot-middleware配合实现HMR

通过插件HotModuleReplacementPlugin()

1.配置
在webpack.config.js中配置devServer

devServer: {
		contentBase: './dist',  // 起一个在dist文件夹下的服务器
		open: true,  // 自动打开浏览器并访问服务器地址
		port: 8085,
                hot: true,      // 开启HMR功能
                 hotOnly: true   // 即使HMR不生效,也不自动刷新
	},
1
2
3
4
5
6
7

pluginsp配置中使用HotModuleReplacementPlugin插件

plugins: [
   ...// 其他插件
    new webpack.HotModuleReplacementPlugin()
    ],
1
2
3
4

2.判断
然后进行手动判断进行模块热更新,如果你不想做以下判断那么可以使用module.hot.accept(),整个项目做hmr只要有代码变化就进行更新。

if(module.hot) {
    module.hot.accept('./number', () => {
      // 使用更新过的 library 模块执行某些操作...
    })
}
1
2
3
4
5

3.启动
最重要一点不要忘了修改启动命令

"start": "webpack-dev-server"
1

此时运行npm start,即可实现模块热更新

通过 Node.js API

通过自己在本地搭建一个服务器,利用webpack-dev-middleware和webpack-Hot-middleware两个插件来配合实现HMR.
1.安装

// 安装express, webpack-dev-middleware , webpack-Hot-middleware
cnpm install express webpack-dev-middleware webpack-Hot-middleware -D
1
2

2.配置dev.server.js
dev.server.js

const path = require('path');
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require("webpack-Hot-middleware")
const config = require('./webpack.dev.js');

const complier = webpack(config);   // 编译器,编译器执行一次就会重新打包一下代码
const app = express();  // 生成一个实例
const {devServer: {port, contentBase}} = config
const DIST_DIR = path.resolve(__dirname, '../', contentBase);  // 设置静态访问文件路径
// 等同于const DIST_DIR = path.resolve(__dirname, '../dist');


let devMiddleware = webpackDevMiddleware(complier, {  // 使用编译器
    publicPath: config.output.publicPath,
    quiet: true, //向控制台显示任何内容
    noInfo: true
})

let hotMiddleware = webpackHotMiddleware(complier,{
    log: false,
    heartbeat: 2000
 })

app.use(devMiddleware)

app.use(hotMiddleware)

// 设置访问静态文件的路径
app.use(express.static(DIST_DIR))


app.listen(port, () => {
    console.log("成功启动:localhost:"+ port)
})  //监听端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

webpack.dev.js配置

module.exports = {
        entry: {             // 入口文件配置
        //实现刷新浏览器webpack-hot-middleware/client?noInfo=true&reload=true 是必填的
        main: ['webpack-hot-middleware/client?noInfo=true&reload=true', './src/index.js']
    },
       devServer: {
        contentBase: 'dist',
        port: 8081
    },
       plugins: [  
        new webpack.NamedModulesPlugin(),  //用于启动HMR时可以显示模块的相对路径
        new webpack.HotModuleReplacementPlugin(), 
        new OpenBrowserPlugin({ url: 'http://localhost:8081' }), // 自动打开浏览器
    ],
    ...// 其他配置
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

完整实现在这里

webpack-hot-middleware的配置项

配置项可以通过query 方式添加到webpack config中的路径来传递给客户端
配置项都有

  • path - 中间件为事件流提供的路径
  • name - 捆绑名称,专门用于多编译器模式
  • timeout - 尝试重新连接后断开连接后等待的时间
  • overlay - 设置为false禁用基于DOM的客户端覆盖。
  • reload - 设置为true在Webpack卡住时自动重新加载页面。
  • noInfo - 设置为true禁用信息控制台日志记录。
  • quiet - 设置为true禁用所有控制台日志记录。
  • dynamicPublicPath - 设置为true使用webpack publicPath作为前缀path。(我们可以webpack_public_path在入口点的运行时动态设置,参见output.- publicPath的注释)
  • autoConnect - 设置为false用于防止从客户端自动打开连接到Webpack后端 - 如果需要使用该setOptionsAndConnect功能修改选项

通过传递第二个参数,可以将配置选项传递给中间件

webpackHotMiddleware(webpack,{
    log: false,
    path: "/__what",
    heartbeat: 2000
})
1
2
3
4
5
  • log - 用于记录行的函数,传递false到禁用。默认为console.log
  • path - 中间件将服务事件流的路径必须与客户端设置相匹配
  • heartbeat - 多长时间将心跳更新发送到客户端以保持连接的活动。应小于客户的timeout设置 - 通常设置为其一半值。
    更多配置在这里webpack-hot-middleware

注意:通过express启动服务器后,devServer中的配置就不起作用了。

3.启动命令

"start": "node ./build/dev-server.js",
1

启动命令npm start,即可实现HMR的功能

HMR实现原理

1.HMR的更新流程

  • 修改了一个或多个文件。
  • 文件系统接收更改并通知Webpack。
  • Webpack重新编译构建一个或多个模块,并通知HMR服务器进行了更新。
  • HMR Server使用websockets通知HMR Runtime需要更新。(HMR运行时通过HTTP请求这些更新。)
  • HMR运行时再替换更新中的模块。如果确定这些模块无法更新,则触发整个页面刷新

2.HMR 工作流程图解
此为更加详细的流程分析:

上图是webpack 配合 webpack-dev-server 进行应用开发的模块热更新流程图。

  • 上图底部红色框内是服务端,而上面的橙色框是浏览器端。
  • 绿色的方框是 webpack 代码控制的区域。蓝色方框是 webpack-dev-server 代码控制的区域,洋红色的方框是文件系统,文件修改后的变化就发生在这,而青色的方框是应用本身
    步骤分析:
  • 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  • 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  • 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  • 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  • webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  • HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  • 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  • 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

sourceMap的工作原理


sourceMap本质上是一种映射关系,打包出来的js文件中的代码可以映射到代码文件的具体位置。例如在打包后有代码错误,这种映射关系会帮助我们直接找到在源代码中的错误

Tree-Shaking

Tree Shaking可以剔除掉一个文件中未被引用掉部分(在producton环境下才会提出),并且只支持ES Modules模块的引入方式,不支持CommonJS的引入方式。原因:ES Modules是静态引入的方式,CommonJS是动态的引入方式,Tree Shaking只支持静态引入方式。
在开发环境下需要在webpack中配置,但是在生成环境下,由于已有默认配置可以不配置optimization,但是sideEffects依然需要配置

Code Spliting

使用Code Spliting可以有两种方式
第一手动实现:
配置

entry: {
        lodash: './src/lodash.js',
        main: './src/index.js'
    }, // 入口文件
1
2
3
4

lodash.js

import _ from 'lodash';
window._ = _;
1
2

第二:使用optimization
同步代码:只需要在webpack中做optimization配置即可
异步代码:无需做任何配置,会自动进行代码分割,放入dist目录中

optimization: {
        splitChunks: {
            chunks: 'all'  // 遇到公用当类库时,自动的Code Spliting
        }
    },
1
2
3
4
5

webpack性能优化

  • 1.跟上技术的迭代更新
  • 2.尽可能少的模块使用loader
  • 3.plugin尽可能精简并保证可靠
    例如尽可能使用官方的插件,并在合适的环境下使用对象的插件
  • 4.resolve参数合理配置
    当通过import child from './child/child'形式引入文件时,会先去寻找.js为后缀当文件,再去寻找.jsx为后缀的文件
resolve: {
   extensions: ['.js', '.jsx'],
  mainFiles: ['index', 'child']// 如果是直接引用一个文件夹,那么回去直接找index开头的文件,如果不存在再去找child开头的文件
  alias: {
   roshomon: path.resolve(__dirname, '../src/child');  // 别名替换,引入roshomon其实是引入../src/child
}
}
1
2
3
4
5
6
7
  • 5.使用DellPlugin提高打包速度
    对于第三方库,只打包分析一次,后面的每次打包都不会重复打包第三方库
    webpack.dll.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
	mode: 'production',
	entry: {
		vendors: ['lodash'],
		react: ['react', 'react-dom'],
		jquery: ['jquery']
	},
	output: {
		filename: '[name].dll.js',
		path: path.resolve(__dirname, '../dll'),
		library: '[name]'
	},
	plugins: [
		new webpack.DllPlugin({ // 使用该插件分析第三方库,并把库里面的映射关系放到[name].manifest.json里,并放在dll文件里
			name: '[name]',
			path: path.resolve(__dirname, '../dll/[name].manifest.json'),
		})
	]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

webpack.common.js

// 引用
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
// plugins配置
....
const plugins = [
	new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), 
	new CleanWebpackPlugin(['dist'], {
		root: path.resolve(__dirname, '../')
	})
];

const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
	if(/.*\.dll.js/.test(file)) {
		plugins.push(new AddAssetHtmlWebpackPlugin({  // 将dll.js文件自动引入html
			filepath: path.resolve(__dirname, '../dll', file)
		}))
	}
	if(/.*\.manifest.json/.test(file)) {
		plugins.push(new webpack.DllReferencePlugin({ // 当打包第三方库时,会去manifest.json文件中寻找映射关系,如果找到了那么就直接从全局变量(即打包文件)中拿过来用就行,不用再进行第三方库的分析,以此优化打包速度
			manifest: path.resolve(__dirname, '../dll', file)
		}))
	}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

package.json

"build:dll": "webpack --config ./build/webpack.dll.js"
1
  • 6.控制包文件大小
    可以通过treeShaking或者拆分文件来优化打包速度
  • 7.thread-loader, parallel-webpack,happywebpack多进程打包
  • 8.合理使用sourceMap
  • 9.结合stats分析打包结果
    通过命令生成一个关于打包情况的stats文件,并借助工具进行打包情况分析,通过分析打包的流程对相应内容进行优化
  • 10.开发环境内存编译
  • 11.开发环境无用插件剔除

AST

抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础
es6转es5
将ES6的代码转换为AST语法树,然后再将ES6 AST转为ES5 AST,再将AST转为代码

babel编译原理


babylon 将 ES6/ES7 代码解析成 AST
babel-traverse 对 AST 进行遍历转译,得到新的 AST
新 AST 通过 babel-generator 转换成 ES5
或者:

  1. 它就是个编译器,输入语言是ES6+,编译目标语言是ES5
  2. babel 官方工作原理
  3. 解析:将代码字符串解析成抽象语法树
  4. 变换:对抽象语法树进行变换操作
  5. 再建:根据变换后的抽象语法树再生成代码字符串

### babel-profilly和babel-transform-runtime的区别 
一、babel-polyfill
由于babel默认只转换新的JavaScript语法,但对于一些新的API是不进行转化的(比如内建的Promise、WeakMap,静态方法如Array.from或者Object.assign),那么为了能够转化这些东西,我们就需要使用babel-polyfill这个插件
由于babel-polyfill是个运行时垫片,所以需要声明在dependencies而非devDependencies里
二、babel-plugin-transform-runtime
由于使用babel-polyfill,会产生以下问题:
1)babel-polyfill会将需要转化的API进行直接转化,这就导致用到这些API的地方会存在大量的重复代码
2)babel-polyfill是直接在全局作用域里进行垫片,所以会污染全局作用域
所以,babel同时提供了babel-plugin-transform-runtime这一插件,它的好处在于:
1)需要用到的垫片,会使用引用的方式引入,而不是直接替换,避免了垫片代码的重复
2)由于使用引用的方式引入,所以不会直接污染全局作用域。这就对于库和工具的开发带来了好处
但是babel-plugin-transform-runtime仍然不能单独作用。因为有一些静态方法,如"foobar".includes("foo")仍然需要引入babel-polyfill才能使用

babel、babel-polyfill的区别


babel-polyfill:模拟一个es6环境,提供内置对象如Promise和WeakMap
引入babel-polyfill全量包后文件会变得非常大。它提供了诸如 Promise,Set 以及 Map 之类的内置插件,这些将污染全局作用域,可以编译原型链上的方法。
babel-plugin-transform-runtime & babel-runtime:转译器将这些内置插件起了别名 core-js,这样你就可以无缝的使用它们,并且无需使用 polyfill。但是无法编译原型链上的方法
runtime 编译器插件做了以下三件事:

  • 当你使用 generators/async 函数时,自动引入 babel-runtime/regenerator 。
  • 自动引入 babel-runtime/core-js 并映射 ES6 静态方法和内置插件。
  • 移除内联的 Babel helper 并使用模块 babel-runtime/helpers 代替。

webpack与grunt、gulp区别

gulp、grunt
gulp和grunt强调的是前端开发的工作流程,我们可以通过配置一系列的task,定义task处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让gulp执行这些task,从而构建项目的整个前端开发流程。
PS:简单说就一个Task Runner
webpack
webpack是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。
PS:webpack is a module bundle
相同功能

  • 文件合并与压缩(css)
  • 文件合并与压缩(js)
  • sass/less预编译
  • 启动server
  • 版本控制

两者区别
虽然都是前端自动化构建工具,但看他们的定位就知道不是对等的。
gulp严格上讲,模块化不是他强调的东西,他旨在规范前端开发流程。
webpack更是明显强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。
总结
gulp应该与grunt比较,而webpack应该与browserify(网上太多资料就这么说,这么说是没有错,不过单单这样一句话并不能让人清晰明了)。
gulp与webpack上是互补的,还是可替换的,取决于你项目的需求。如果只是个vue或react的单页应用,webpack也就够用;如果webpack某些功能使用起来麻烦甚至没有(雪碧图就没有),那就可以结合gulp一起用

最后更新时间: 10/8/2019, 7:41:17 PM