入门
先简单地提下模块化的思想。
模块化
简单来说就是将复杂的系统分解成各个简单的子模块,便于开发和维护。
一般 JavaScript 模块化规范有 CommonJS,AMD 和 ES6 中的模块化。
CommonJS
其核心思想就是利用 require
来同步加载依赖的模块,通过 module.exports
来暴露模块的接口。
优点在于
- 代码可在 node.js 环境下运行
- NPM 包中大部分模块都支持 CommonJS
缺点在于:无法在浏览器环境下运行,要想运行必须通过工具转换。
AMD
异步模块定义(Asynchronous Module Definition) 是以异步的方式加载模块,主要用于解决浏览器的模块化加载,比如像 require.js.
优点
- 可无需转换即可在浏览器环境中运行
- 可异步加载模块
- 可并行加载多个模块
- 可在node和浏览器环境中运行
缺点:浏览器没有原生支持 AMD,若要使用需要导入相应的包。
ES6 的模块化
它是 ECMA 提出的 JavaScript 模块化规范,是在语言层面上实现的。他将逐渐取代 CommonJS 和 AMD 规范。但是目前仍然无法运行在大部分 JavaScript 运行环境中,所以需要转换工具转成 ES5 后才能运行。
tip: CSS 样式文件中也开始支持模块化,比如 SASS, SCSS, less
等
不仅这些模块化规范需要转换工具才能运行,还有目前流行 React 中 JSX 语法和 vue 中的 template 也需要进行转换,还有当前流行 es6,typescript 也需要转换。所以随着项目越来越复杂和技术越来越新,不可避免的需要出现构建工具来统一的管理这些转换工具。
常见的构建工具
构建工具主要的工作如下:
- 代码转换
- 文件优化:压缩静态资源文件
- 分割代码,将公共代码分离出来
- 模块合并
- 实现热更新
- 自动校验
- 实现自动发布
主要的构建工具有(诞生的时间排序):
- Npm Script:属于 NPM 内置的功能,在 package.json 文件中的 script 对象中配置,其中每个属性对应一个 shell 命令。
- Grunt:通过执行任务的方式进行构建。
- Gulp:一种基于流的自动化构建工具,除了可以管理和执行任务,还能监听文件,读写文件。
- Webpack:专注于构建模块化项目,在 webpack 里,一切文件都被视为模块。通过 loader 转换文件,通过 Plugin 注入钩子,最后输出有多个模块组合而成的文件。
- Rollup:和 webpack 类似,他的亮点就是可以 Tree shaking,以减小输出文件大小和提高运行性能。后期 webpack 也是实现了 Tree Shaking。
选择 Webpack 的原因
- 可以为新项目提供一站式的解决方案
- 有良好的生态和维护团队
- 被大量使用,经受住了大家的考验。
安装
# 初始化 npm 包
npm init
# 以下三选一
# 稳定版
npm install webpack -D
# 指定版
npm install webpack@<version> -D
# 最新的体验版
npm install webpack@beta -D
请看以下 webpack 使用实例(文件都在根目录)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>
<!-- 导入webpack打包输出的文件 -->
<script src="./dist/bundle.js"></script>
</body>
</html>
show.js
function show(content){
document.getElementById("app").innerText = "hello, " + content;
}
// 通过 CommonJS 规范导出show函数
module.exports=show;
main.js
// 通过 CommonJS 规范导入show函数
const show = require('./show.js')
show("webpack")
wepack.config.js
const path = require('path')
module.exports = {
// javascript 执行入口文件
entry: './main.js',
output: {
// 将所有依赖的模块合并输出到bundle.js文件中
filename: "bundle.js",
// 将输出文件都放到此目录下
path: path.resolve(__dirname, "./dist")
}
}
然后在根目录执行打包命令 webpack
会看到生成 dist 目录,里面会有打包好的文件bundle.js。
打开 index.html 文件便可看到构建效果。
从 Webpack 2版本开始,就已经内置了转换 CommonJS、ES6、AMD模块化的功能
简单的 Loader 使用示例
假如我们需要给页面添加样式,新建了 main.css
文件:
#app{
color:red;
}
为了让样式生效,我们还需要在 main.js
中导入:
// 通过 CommonJS 规范导入 main.css
require('main.css')
// 通过 CommonJS 规范导入show函数
const show = require('./show.js')
show("webpack")
因为 webpack 原生是不支持非 JavaScript 文件转换的,所以直接通过webpack
打包命令可能会报错:
ERROR in ./main.css 1:0
Module parse failed: Unexpected character '#' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> #app{
| color: red;
| }
@ ./main.js 2:0-21
由此需要引出 Loader 概念,它就是用来处理文件转换的。如果需要支持 CSS 模块的转换,首先需要先安装相关 Loader:
npm install -D style-loader css-loader
之后在 webpack.config.js
文件中配置:
const path = require('path')
module.exports = {
// javascript 执行入口文件
entry: './main.js',
output: {
// 将所有依赖的模块合并输出到bundle.js文件中
filename: "bundle.js",
// 将输出文件都放到此目录下
path: path.resolve(__dirname, "./dest")
},
// 配置 Loader
module: {
rules: [
{
test:/\.css$/,
// css-loader负责读取css文件,再通过 style-loader 注入到js中
use:['style-loader', 'css-loader?minimize']
}
]
}
}
通过在 module.rules
数组中配置一系列规则,以便告知 webpack 根据哪些 Loader 去转换指定的文件。其中 use
属性需要注意以下几点:
-
Loader 的执行顺序是由后向前的。
-
每个 Loader 传参方式有两种:
-
通过 URL querystring 的方式,比如
css-loader?minimize
就是告知 Loader 要开启 CSS 压缩。 -
通过options 方式:
const path = require('path') module.exports = { // javascript 执行入口文件 entry: './main.js', output: { // 将所有依赖的模块合并输出到bundle.js文件中 filename: "bundle.js", // 将输出文件都放到此目录下 path: path.resolve(__dirname, "./dest") }, module: { rules: [ { test:/\.css$/, use:['style-loader', { loader:'css-loader', options: { minimize:true } }] } ] } }
-
注:webpack3.0 和 css-loader1.0 以上就不支持 minimize 了,为了讲解传参方式,还是用了它。
简单的 Plugin 使用示例
Plugin 是用来扩展 webpack 功能的,通过在构建流程中注入钩子实现。比如我们需要将 CSS 从 bundle.js 文件中分离出来,可以用 extract-text-webpack-plugin
,首先还是先安装 Plugin:
npm install extract-text-webpack-plugin -D
然后在 webpack.config.js 文件中配置:
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
let plugins = []
plugins.push(new ExtractTextPlugin({
// 提取出来的 css 文件名
filename: `[name]_[contenthash:8].css`
}))
module.exports = {
// javascript 执行入口文件
entry: './main.js',
output: {
// 将所有依赖的模块合并输出到bundle.js文件中
filename: "bundle.js",
// 将输出文件都放到此目录下
path: path.resolve(__dirname, "./dist")
},
module: {
rules: [
{
test:/\.css$/,
use:ExtractTextPlugin.extract({
fallback:'style-loader',
use:['css-loader']
})
}
]
},
plugins
}
**注:webpack4.0 以上就不再支持 extract-text-webpack-plugin **,可以通过安装新的版本解决:npm install --D extract-text-webpack-plugin@next
使用 DevServer
一般我们开发网页调试有以下几个需求:
- 提供 http 服务而不是不是本地文件预览
- 监听文件内容变化,实时更新页面
- 提供 sourcemap 功能,以便调试页面
后两个功能 webpack 原生支持。第一个需求可使用 webpack-dev-server
,它可以启动 http 服务用于网络请求,并同时执行 webpack 命令,将构建后的文件存储在内存中。所以执行 webpack-dev-server
命令是不会创建 dist
目录,所以在 .index.html
中静态资源路径需要将以前的./dist/bundle.js
修改为 ./bundle.js
。
第二个功能可以通过给 webpack-dev-server
命令加 --hot
参数实现:
也就是常说的热替换,在不重新刷新页面的情况下通过替换旧模块来更新页面。
webpack-dev-server --hot
第三个功能可以通过给 webpack-dev-server
命令加 --devtool source-map
参数实现:
webpack-dev-server --devtool source-map
核心概念
以下几个核心概念需要清楚:
- Entry:入口
- Module:模块
- Chunk:代码块
- Loader:模块转换器
- Plugin:扩展插件
- Output:输出结果
webpack 的流程:
webpack 启动时会先找到 Entry 配置里 Module,去解析 Entry 依赖的所有 Module。每找到一个 Module 就会根据 Loader 里的相应规则进行转换,转换后会再次解析当前 Module 依赖的所有 Module,这些模块会以 Entry 为单位进行分组,一个 Entry及其依赖的所有 Module 形成一个组,也就是一个 chunk,webpack 会将所有的 chunk 转换成输出文件。在构建中,webpack会在恰当的时机执行 plugin 中的逻辑。
配置
webpack 配置总的来说有两种方式,一种通过配置文件webpack.config.js
,另一种则是通过命令行的方式,如webpack --devtool map-resource
本章主要讲解下以下配置概念:
- Entry:配置模块的入口文件
- Output:配置构建输出文件信息
- Module:配置解析模块的规则
- Resolve:配置寻找模块的规则
- Plugins:配置扩展插件
- DevServer:配置 DevServer
- 配置总结
Entry
context
webpack 在处理相对路径时会以 context
为根目录,context
默认为当前配置文件目录,淡然也可以通过以下两种方式修改:
- 修改配置文件webpack.config.js
module.exports={
context:path.resolve(__dirname, 'app')
}
- 命令行式改变
webpack --context path
Entry 类型
entry可以是三种类型:
- String:
'./app/entry'
,只输出一个Chunk,Chunk的名称是main - Array:
['./app/entry1', './app/entry2']
,只输出最后一个元素的 Chunk,Chunk的名称是main - Object:
{a:'./app/entry-a',b:'./app/entry-b'}
,输出多个Chunk,Chunk的名称是对象对应的键值
动态配置 Entry
当入口文件是动态时,就需要给 Entry 传入函数,如下配置:
// 同步函数
entry:()=>{
return {
a:'./app/entry-a',
b:'./app/entry-b'
}
}
// 或者异步函数
entry:()=>{
return new Pormise((resolve)=>{
resolve({
a:'./app/entry-a',
b:'./app/entry-b'})
})
}
Output
常见的属性有:
filename
:配置输出文件的名称path
:配置输出文件的目录publicPath
:用于处理静态资源引用地址
filename 和 path
比如:
output:{
filename:'bundle.js',
path:path.resolve(__dirname, './dist')
}
filename
除了可以配置静态名称也可以配置动态名称:
filename: [id].js
:chunk的唯一标识符,从0开始filename: [name].js
:chunk的名称filename: [hash].js
:chunk 唯一标识hash,可以取hash前几位,比如取前8位:filename: [hash:8].js
filename: [chunkhash].js
:chunk 内容的 hash,可以取hash前几位,比如取前8位:filename: [hash:8].js
其他属性
chunkFilename
publicPath
crossOriginLoading
libraryTarget
和 librarylibraryExport
- 等等…
Module
主要用来配置模块处理功能,里面主要有 rules
属性,Array 类型,用于加载和解析模块文件,用于配置 Loader 规则。
rules
元素有以下几个常用的属性:
- test
- use
- noParse
- parser
配置 Loader
通过以下的示例来说明 Loader 配置。
module: {
rules: [
{
// 命中 js 文件
// test 也可以接收数组类型,其中每个元素条件之间是“或”的关系
test:/\.js$/,
// 用 babel-loader 转换 JavaScript 文件
// ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 的便宜结果,加快编译速度
// use 数组里的loader执行顺序为从右向左,也可以通过enforce强制设置执行位置。
use:['babel-loader?cacheDirectory'],
// 只命中 src 目录里的 JavaScript 文件,加快 Webpack 的搜索速度
include:path.resolve(__dirname,'src'),
// 通过 exclude 排除 node_modules 目录下的文件,
// 一般来讲,include和exclude不必要同时配置,在此为了说明两者用法,故同时配置
exclude: path.resolve(__dirname,'node_modules'),
// 配置哪些内置模块语法被解析
parser: {
amd:false, // 禁用 AMD
commonjs:false, // 禁用 CommonJS
system:false, // 禁用 SystemJS
harmony:false, // 禁用ES6 import/export
// 等等...
},
}
],
// 忽略符合以下规则模块的解析, noparse 可以是 RegExp,[RegExp],function中的一种
noParse: /jquery|chartjs/
}
Resolve
用来告知 webpack 按照何种规则来寻找模块。
可由以下示例来讲解:
resolve: {
// 配置别名
alias: {
'@':'./src/components',
},
// 决定优先是哪个入口文件代码,查找顺序从左至右
mainFields: ['browser','main'],
// 配置扩展名,当导入文件的扩展名省略时,从以下数组中依次匹配查找,查找顺序从左至右
extensions: ['.ts','.js','.json'],
// './src/components' 和 'node_modules' 路径下的模块导入的相对路径可以其作为根目录
// 比如 './src/components' 的button模块,直接可以通过 import 'button' 导入
modules:['./src/components', 'node_modules']
// 配置第三方模块文件名称描述,也就是 package.json
descriptionFiles: ['package.json'],
// 模块导入时是否必须带扩展名
enforceExtension: true,
// 模块导入时是否必须带扩展名,只针对 node_modules 下的模块生效
enforceModuleExtension: false
},
Plugin
Plugin 用于扩展 webpack,配置很简单,接收数组类型,每个元素都是 Plugin的示例,Plugin的参数由构造函数传入,比如:
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
plugins:[new ExtractTextPlugin({
// 提取出来的 css 文件名
filename: `style.css`
})]
}
难点在于各自 Plugin 自身的参数配置。
DevServer
DevServer 除了在命令行配置参数,也可以在配置文件webpack.config.js中配置
module.exports={
devServer: {
host:'127.0.0.1',
port:8080
}
}
此配置项仅针对 devServer
命令有效,webpack
命令会忽略此配置项。
之前也讲了它的简单用法,以下讲下其常用的配置:
hot
:用于热替换host
:用设置web服务器地址port
:用于设置端口号https
:用于设置https服务,默认是http服务。open
:在第一次构建完是否自动打开网页
其他配置项
devtool
:用于设置 source-map,便于调试watch
和watchOptions
:用于监听文件改动,webpack-dev-server
命令自动开启externals
:告知以下文件无需打包。- 等等…
实战
具体配置可查看书籍或相关Loader介绍。
优化
优化方向有两个:
- 优化开发效率(针对开发人员)
- 提高构建速度
- 优化开发体验
- 优化输出质量(针对用户)
- 减少首屏加载时间
- 提升流畅度
缩小文件的搜索范围
- 优化 Loader 配置:针对
module
配置- 可以添加 noParse 属性,排除一些非模块化文件,像
noparse:/jquery|chart\.js/
。确保这些文件中没有模块化操作。 - 在 rules 属性中给其 Loader 尽可能添加 include 或 exclude 属性以缩小解析范围。
- 可以添加 noParse 属性,排除一些非模块化文件,像
resolve
配置:extensions
的数组长度尽可能的短,频率高的后缀名放前面。- 尽可能的在
alias
中配置依赖模块的具体位置。 - 等等…
使用 HappyPack
使用 HappyPack 插件进行多进程构建。它会接管某些 Loader 任务,并行执行这些任务。
使用 parallelUglifyPlugin
插件
webpack 内置了 UglifyPlugin
用于压缩代码,parallelUglifyPlugin
插件可以多进程并行压缩代码。
使用自动刷新优化开发体验
主要通过配置 watch
实现。
监听文件是否发生改变,如果发生改变则在一定时间内重新构建输出文件,并通过向网页中注入代理客户端代码的方式来实现自动刷新功能。
开启模块热替换优化开发体验
原理:只需编译发生变化了的代码,再将新的输出模块替换浏览器中旧的模块。
优势:
- 构建速度快。
- 可以在保存浏览器状态的情况下更新页面。
区分环境
通过 DefinePlugin
插件来定义 process.env.NODE_ENV
的值。
压缩代码
JavaScript 压缩原理:插件通过分析 JavaScript 语法分析树,按照一定的规则将代码中的输出日志、注释等一些无用代码去除。
CSS 压缩原理:理解 CSS 含义,比如会将 color:#ff0000
压缩成 color:red
,压缩率可达到 60%。
- 压缩 ES5
- 可用之前说的
UglifyPlugin
和ParallelUglifyPlugin
插件进行配置,也可以启动webpack
时带上--optimize-minimize
参数,它会自动配置UglifyPlugin
插件的默认参数。
- 可用之前说的
- 压缩 ES6
- 可用
uglifyESPlugin
插件进行压缩,需要注意的是压缩 ES6 代码需要运行环境支持ES6语法才有意义。
- 可用
- 压缩 CSS
- 通过给
css-loader
Loader添加minimize
参数进行压缩:use:['css-loader?minimize']
- 通过给
CDN 加速
将不同的静态资源放在不同域名的CDN下,可以利用webpack
进行配置
使用 Tree Shaking
Tree Shaking 插件是为了剔除没有用到的死代码。
使用 Tree shaking 的前提是模块化必须是 ES6 语法,因为其导入导出的路径必须是静态字符串,不能出现在代码块中。像 CommonJS 就不一样:require(x+y)
提取公共代码
为什么要提取?
因为如果每个页面都包含大量公共代码的话,会导致:
- 相同的资源被重复加载,浪费用户流量和服务器成本
- 每个页面需要加载的资源过大,影响页面渲染
如何提取?
先将所有页面中用到的基础依赖库(如react,react-dom)提取到单独的一个文件 base.js 中。然后再从所有页面中提取出不包含base.js中代码的公共代码到common.js文件中,然后将每个网页剩余的代码单独包装成一个文件。
如图:
提取公共代码可利用 webpack 内置的 commonsChunkPlugin 插件
分割代码以按需加载
像现在流行的单页应用,将所有的功能整合到了一个 HTML 文件中。其实每次只是运用到其中的一部分功能,为了使用一部分功能而加载所有资源而使得网页加载相对缓慢。为了解决此问题,则可以将整个页面分成一个个小的功能,而根据功能之间的相关程度进行分类,把每一类合并成一个chunk,然后再按需加载每一个Chunk。
可以使用 import(*)
语句实现按需加载。
使用 Prepack
通过深入分析JavaScript代码逻辑,预先执行代码逻辑,然后将执行结果输出,此技术还未成熟。
比如:
// 转换前
function hello(){
console.log("hello,world")
}
hello();
// 转换后
console.log("hello,world")
开启 Scop Hoisting
可以进一步的减少打包体积,通过将多个函数进行合并,但是前提是运用ES6语法的模块。
可利用 webpack 内置的功能进行开启
输出分析
如果需要分析webpack 的详细输出信息,可在命令行添加以下参数
--profile
:记录构建过程中的耗时信息--json
:以 json 格式记录输出文件信息
如:webpack --profile --json > state.json
>
为 LINUX 中的管道命令,就是将输出结果存到 state.json
文件中
可以通过以下两种方式来可视化分析 state.json
文件信息
- Webpack Analyse:一个web应用,登录其官网上传 json 文件即可分析
- webpack-bundle-analyzer:一个全局插件,在
state.json
所在目录下输入webpack-bundle-analyzer
即可分析state.json
。
原理
工作原理概括
核心概念
- Entry:构建入口。
- Ouput:构建输出信息。
- Module:模块,在 webpack 中每个文件都当作是一个模块。
- Chunk:代码块,有多个模块组成,用于代码合并与分割。
- Loader:模块转换器,Loader 根据需求将原有文件转成新的内容。
- Plugin:Webpack 扩展插件,webpack 在构建的过程中会广播一些事件,而 Plugin 则通过监听相应的事件来扩展某些功能。
构建流程
主要分三大阶段:
- 初始化:启动构建,读取合并 webpack 配置参数,加载插件,初始化 compiler 对象。
- 编译:读取 Entry 配置中的模块,根据匹配规则调用相应的 Loader 对其进行转换,并找到其依赖的模块进行递归解析转换,并确定各个模块之间的依赖关系。
- 输出:将有联系的多个Module合并成一个chunk,再将每个chunk输出成一个文件并保存在系统中,保存信息由 Output 配置决定。
编写 Loader
简单来说输出一个转换函数。
Npm Link 可用于调试开发本地的 Npm 模块。
编写 Plugin
以下有些常用的属性和function总结如下:
compilation.chunk
:数组类型,存放所有的chunk
chunk.forEachModule(module=>{})
:遍历每个module
module.fileDependencies
:数组类型,表示当前模块所依赖的文件路径chunk.files
:数组类型,表示chunk
输出的文件,有可能不只一个文件。compilation.assets[filename].source()
:表示filename
输出文件的内容compilation.fileDependencies
:数组类型,表示文件依赖列表compiler.options.plugins
:当前配置使用的所有插件列表