模块化开发的趋势越来越明显,特别是在大型前端项目中,模块化的开发让我们更好地组织和更灵活地使用代码,在Node端使用的是CommonJS规范的模块化,在浏览器端则是使用ES Modules模块规范。由于ES Modules是ES6才提出的,目前仅有少数版本的浏览器实现了模块化,如果我们使用模块化进行开发,就需要考虑浏览器兼容问题。另外,模块化的代码虽然在开发阶段对开发友好,但过多的模块并不利于浏览器的频繁加载调用。因此,为了解决模块化开发中的兼容和代码组织问题,我们一般使用模块打包工具来为我们实现模块代码打包为生产代码。
目前主流的模块打包工具有:
本文主要就webpack
的使用进行展开说明。
实际上,模块打包工具除了解决上面所说的模块规范兼容问题,还能优化组织其他资源的使用,**不仅限于js
**,就webpack而言,理论上,我们应当把所有的资源文件都当成独立的模块去加载,去使用,而不仅仅是处理js模块代码。
快速上手
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph)*,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 *bundle。
从 webpack v4.0.0 开始,可以不用引入一个配置文件。然而,webpack 仍然还是高度可配置的。在开始前你需要先理解四个核心概念:
- 入口(entry)
- 输出(output)
- loader
- 插件(plugins)
无配置(默认配置)打包
为了方便测试我们先创建一个一些基本的文件进行测试:
1 | <!-- dist/index.html --> |
1 | // src/header.js |
1 | // scr/index.js |
写好了测试文件,我们安装webpack即可使用:
1 | npm init -y |
在4.0以上的版本我们可以不使用配置文件直接运行打包指令:
1 | yarn webpack |
然后在dist
目录就会出现一个main.js
,这个js就是打包后的js代码,打开index.html
文件就能看到执行的效果。
可以发现,默认配置的打包入口为
scr/index.js
,出口为dist/mian.js
文件。
使用配置文件 - 基本的四个核心配置
虽然我们可以使用默认配置直接打包,但通常我们都会根据自己的需求去修改配置文件webpack.config.js
。
(当然你也可以声明为别的文件名,然后用--config **.js
的打包参数来指定打包文件名)
入口entry
上面说过,webpack的打包是递归地构建模块依赖图的过程,而入口的配置,就是指定webpack应该使用哪个或哪些模块作为递归的入口模块。
比如我们以src
目录下的main.js
作为打包入口文件:
1 | module.exports = { |
或者使用多入口文件:
1 | module.exports = { |
出口output
出口即递归构建的最终结果,一个入口对应一个出口文件,一般用于指定打包的路径和文件名:
1 | const path = require('path') |
当通过多个入口起点(entry point)、代码拆分(code splitting)或各种插件(plugin)创建多个 bundle,应该使用以下一种替换方式,来赋予每个 bundle 一个唯一的名称……
1 | // 使用入口名称 |
加载器loader
文件加载器是webpack构建的核心部分,我们在上文说过,webpack中,不只是js被当做模块来导出,其他资源同样需要会当做模块来打包处理。
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
在更高层面,在 webpack 的配置中 loader 有两个目标:
test
属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。use
属性,表示进行转换时,应该使用哪个 loader。
比如:
1 | const path = require('path'); |
以上配置中,对一个单独的 module 对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。这告诉 webpack 编译器(compiler) 如下信息:
“嘿,webpack 编译器,当你碰到「在
require()
/import
语句中被解析为 ‘.txt’ 的路径」时,在你对它打包之前,先使用raw-loader
转换一下。”
更多loader使用细节请参考:https://www.webpackjs.com/concepts/loaders/。
插件plugins
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
想要使用一个插件,你只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建它的一个实例。
下面是一个自动创建html文件的插件:html-webpack-pugin
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); |
点击这里了解更多!。
模式
在实际开发中,我们一般有两种环境的代码:开发环境和生产环境。生产环境的代码应该更接近源码,以便于我们调试和定位错误源,而生产环境一般需要压缩和更优的代码组织方式,webpack提供了一个mode
字段进行模式的切换,并内置了一些基本的打包配置,通过选择 development
或 production
之中的一个,来设置 mode
参数,你可以启用相应模式下的 webpack 内置的优化:
1 | module.exports = { |
或者在命令行中使用指定:
1 | yarn webpack --mode production |
点击这里了解更多!。
webpack打包运行原理
为了兼容各浏览器,模块化打包之后其实并未使用es module
的语法,而是基本的es
语法,“与模块化无关”,只是功能上实现的模块化的效果。
比如我们打包两个模块文件:main.js
,header.js
,具体代码如下:
1 | // main.js |
1 | // header.js |
打包后的bundle
为:
1 | (() => { |
我们发现,整个代码都使用了立即执行的匿名函数来执行,所有的模块代码也就变成了内部函数,也就无法从window
环境下修改和访问模块代码,保证了模块代码的独立性。
在立即执行函数内部,代码可分为两个部分:模块代码的注册,模块代码的引用。
模块代码的注册
注册模块代码的机制很简单,使用一个对象来保存即可,在上面的打包代码中就是,__webpack_modules__
这个变量,模块代码使用了文件相对路径作为模块的key
,值为下面格式的接口(函数):
1 | /** |
这样,我们返回模块的exports对象就可以使用到模块内部定义的成员变量了。
模块代码的引用
上面说到模块代码的注册,我们可以通过模块的key
得到模块的注册函数,这样,传入exports
的引用,我们也就拿到了被引用模块的成员变量,具体的逻辑是这样的:
1 | function __webpack_require__(moduleId) { |
这样,我们只需要在调用手动加载一下模块入口文件,即可自动触发模块之间的调用链:
1 | __webpack_require__("./src/main.js"); |
各种资源加载器的使用·Loader
loader的运行机制
loader是webpack中最基本,也是最核心的打包机制之一,我们知道,webpack默认只提供了js的模块化方案,但是webpack打包希望我们将所有其他资源也看作是模块进行打包,而这里的其他资源,说的就是js资源外的其余资源,webpack相应提供了各种资源的模块化加载loader,我们可以按需进行安装使用。更多细节请参考官方说明文档。
loader的作用有很多,但本质上就是将指定匹配的文件输出为loader处理后的数据,可以理解为文件格式(数据)转换工具。
下面我们自定义一个loader,来感受其中的变化过程。
假设我们有这样一个readme.md
文件作为主页:
1 | ## about me |
我们需要加载.md
文件作输入数据,我们可能有这样的写法:
1 | import readme from './readme.md' |
直接webpack
打包命令会发现执行报错,这是因为md
不是js语法,如果直接打包到js中就会产生报错。
所以我们需要使用loader
将md
文本转换为js代码段,这样才能正常插入。
我们定义一个markdown-loader
:
1 | exports.default = function(source) { |
source
接收数据来源,经过一系列转换后得到最终的模块化代码。
我们看一下最终的界面效果:
我们发现,md文本数据正常被页面加载了,现在我们借助第三方模块markdown
来讲md语法解析为真正的html代码,
1 | const toHTML = require('markdown').parse |
然后再次编译打包查看界面效果:
当然,如果我们使用多个loader进行加载,比如使用html-loader
:
1 | { |
那么,我们上面写的markdown-loader就可以直接返回处理过的source字符串交给下一个loader:
1 | const marked = require('marked') |
关于如何编写一个loader,这里有更多的示例。
文件相关loader
raw-loader
用于加载文件的原始内容(utf-8)。比如用于读取txt
纯文本。val-loader
将代码作为模块执行,并将其导出为 JS 代码。url-loader
与file-loader
类似,但是如果文件大写小于一个设置的值,则返回data URL。比如某些图片资源,本身很小,我们就可以使用
url-loader
转换为data url到页面中。1
2
3
4{
test: /\.([png|jpg|svg|svg|webp]$)/,
use: 'url-loader',
}1
2
3
4
5import google from '../public/images/google.webp'
const img = document.createElement('img')
img.src = google
document.body.append(img)然后在bundle.js中就可以看到图片被解析为了
data url
格式,并作为模块导入。当然,如果图片太大,不应该解析为base url,这样会让bundle文件提及变大,所以可以增加
limit
选项来控制文件的最大转换值,此外,如果转换失败,我们还需要指定fallcall
的失败后的loader
:1
2
3
4
5
6
7
8
9
10{
test: /\.(png|jpg|gif|webp)$/,
use: {
loader: 'url-loader',
options: {
limit: 5 * 1024, // 5k,
fallback: require.resolve('file-loader'), // 转换失败或超过limit,则使用file-loader进行加载
}
},
}file-loader
将文件保存至输出文件夹中并返回(相对)URL。file-loader是普通引用资源的加载器,首先file-loader会将require的数据转换为
url
并输出文件数据到输出目录,本质上就是一次文件的拷贝。1
2
3
4
5
6
7
8
9
10{
test: /\.(png|jpg|gif|svg)$/,
use: {
loader: 'file-loader',
options: {
name: '[contenthash].[ext]',
outputPath: 'images',
}
},
}ref-loader
用于手动建立文件之间的依赖关系
JSON相关loader
json5-loader
加载并转换 JSON 5 文件webpack.config
1
2
3
4
5{
test: /\.(json|json5)$/,
loader: 'json5-loader',
type: 'javascript/auto',
},userInfo.json
1
2
3
4{
"name": "Jinx",
"age": 24
}entry.js
1
2
3import userInfo from '../public/userInfo.json'
document.body.innerHTML = `Hello, I'm ${userInfo.name}, I'm ${userInfo.age} years old.`cson-loader
加载并转换 CSON 文件
语法转换Loader
buble-loader
使用 Bublé 加载 ES2015+ 代码并将其转换为 ES5traceur-loader
使用 Traceur 加载 ES2015+ 代码并将其转换为 ES5ts-loader
像加载 JavaScript 一样加载 TypeScript 2.0+coffee-loader
像加载 JavaScript 一样加载 CoffeeScriptfengari-loader
使用 fengari 加载 Lua 代码elm-webpack-loader
像加载 JavaScript 一样加载 Elm
这里我们学习使用babel-loader
和ts-loader
:
babel-loader
老生常谈的东西了,安装必要的模块:
1 | npm i babel-loader @babel/core @babel/preset-env --dev |
webpack.config
1 | { |
ts-loader
如果你的项目使用了ts
作为开发语言,那么打包时就需要解析为js
语言。
1 | npm ts-loader typescript --dev |
webpack.config.js
1 | const HtmlWebpackPlugin = require('html-webpack-plugin') |
src/main.ts
1 | import { Person } from "./interface"; |
src/interface.ts
1 | export interface Person { |
为了让ts解析能正常工作,我们还需要在项目根目录新增tsconfig.json
文件:
1 | { |
更多配置请参考:编译选项
模板Loader
html-loader
将 HTML 导出为字符串,需要传入静态资源的引用路径pug-loader
加载 Pug 和 Jade 模板并返回一个函数markdown-loader
将 Markdown 编译为 HTMLreact-markdown-loader
使用 markdown-parse 解析器将 Markdown 编译为 React 组件posthtml-loader
使用 PostHTML 加载并转换 HTML 文件handlebars-loader
将 Handlebars 文件编译为 HTMLmarkup-inline-loader
将 SVG/MathML 文件内嵌到 HTML 中。在将图标字体或 CSS 动画应用于 SVG 时,此功能非常实用。twig-loader
编译 Twig 模板并返回一个函数remark-loader
通过remark
加载 markdown,且支持解析内容中的图片
主要为html模板和markdown模板。
html-loader
webpack.config.js
1 | { |
temp.html
1 | <div> |
main.ts
1 | import temp from './temp.html' |
注意,ts默认是不支持导如html文件的,所以我们需要使用模块声明语句进行声明:
html.d.ts
:
1 |
|
如果未生效记得把改文件手动加入
tsconfig.json
配置文件include
字段下。
html-loader还有很多别的用法,需要配合不同的配置,这里就不再逐一介绍。
样式Loader
style-loader
将模块的导出作为样式添加到 DOM 中css-loader
解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码less-loader
加载和转译 LESS 文件sass-loader
加载和转译 SASS/SCSS 文件postcss-loader
使用 PostCSS 加载和转译 CSS/SSS 文件stylus-loader
加载和转译 Stylus 文件
style-loader & css-loader
如果css文件很少,或者说希望像模块一样导入css文件,我们就可以把css文件转化为style节点,添加到对应的页面中去。通常情况下,style-loader和css-loader是联合使用的。
1 | npm i style-loader css-loader --dev |
main.css
1 | p { |
main.ts
1 | import temp from './temp.html' |
用法1:生成style节点:
1 | { |
用法2:生成link节点:
1 | { |
less-loader
如果项目中使用了less语法进行样式编译,我们再使用less-loader作为一级loader,把处理过的less文件交给css-loader和style-loader即可。
1 | // webpack.config.js |
Linting和测试Loader
mocha-loader
使用 mocha 测试(浏览器/NodeJS)eslint-loader
PreLoader,使用 ESLint 清理代码jshint-loader
PreLoader,使用 JSHint 清理代码jscs-loader
PreLoader,使用 JSCS 检查代码样式coverjs-loader
PreLoader,使用 CoverJS 确定测试覆盖率
eslint
1 | npm i eslint eslint-loader --dev |
1 | module.exports = { |
框架Loader
vue-loader
加载并编译 Vue 组件polymer-loader
使用支持配置的预处理程序处理 HTML 和 CSS,并使用require()
加载模块的方式处理 Web Componentsangular2-template-loader
加载并编译 Angular 组件
常用插件的使用·Plugins
webpack 有着丰富的插件接口(rich plugin interface)。webpack 自身的多数功能都使用这个插件接口。这个插件接口使 webpack 变得极其灵活。
除了loader
,webpack另一个核心特性就是Plugins插件的使用,各种loader让我们能将引用资源模块化,而plugins可以让满足我们模块化开发之外的需求,比如动态创建html文件、清除文件、压缩代码等等。
用法
webpack插件是一个实现了apply
方法的对象。appply
方法会被webpack coimpiler调用,并且在整个编译生命周期都可以访问compiler对象。
比如:ConsoleLogOnBuildWebpackPlugin.js
1 | const pluginName = 'ConsoleLogoOnBuildWebpackPlugin' |
由于插件可以携带参数,所以在webpack配置中,必须向plugins
属性传入一个new
实例。
一般有两种用法,
使用配置文件使用插件
webpack.config.js
1 | { |
使用node api的方式使用插件
some-node-script.js
1 | const webpack = require('webpack') // 访问 webpack 运行时(runtime) |
compiler是webpack的支柱引擎 ,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例。它扩展(extend)自
Tapable
类,以便注册和调用插件。大多数面向用户的插件首,会先在Compiler
上注册。
从上面的使用案例,我们可以发现,webpack的插件机制就是简单的钩子机制
,通过webpack的compiler钩子来选择性地在各个编译阶段执行你需要的插件功能。
自动生成html:html-webpack-plugin
HtmlWebpackPlugin
简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。
使用尤其简单,你还可以在模板html文件的基础上进行扩展:
1 | plugins: [ |
自动清除目录文件:clean-webpack-plugin
如果你有清除目录的习惯,特别是常常切换打包模式而导致打包目录下文件不同的时候,自动清除构建目录也许会很方便。
1 | npm i clean-webpack-plugin --dev |
1 | plugins: [ |
开发一个自定义插件
插件的使用是根据需求来决定的,webpack提供了跟多插件,也有很多优秀的第三方webpack插件,我们可以按需根据关键字检索。当然,如果某些插件无法满足你的特定需求,你可以自己动手编写一个插件,因为我们已经知道,一个webpack插件运行的基本原理只需要满足:
- 使用
compiler
对象作为编译时的钩子。 - 实现一个
apply
方法。
我们尝试开发一个js beautify
的插件,
public/plugins/js-beautify-plugin
:
1 | const beautify = require('js-beautify') |
配置后执行就可以看到效果了。
wabpack开发体验优化
自动编译
自动编译基本就是通过监听文件变化来重新打包编译实现。而要开启自动编译功能,只需要开启watch
模式即可:
1 | yarn webapck --watch |
自动更新浏览器 & Webpack Dev Server
我们可以使用类似browserSync
这样的工具开启服务器,但如果要实现自动监听还需要另外开启文件的监听,这样就导致了重复了监听任务,所以这里推荐使用webpack官方的webpack-dev-server
插件,这样我们在开启watch
模式的同时就会自动开启浏览器的自动刷新。
需要注意的是,很多时候安装运行由于版本不匹配导致报错,可以尝试使用下面的版本:
1 | { |
webpack-dev-server
还有一些配置可供选择:
1 | { |
更多配置请参考:devServer。
Source Map
由于webpack将项目模块打包编译之后,代码的位置已经发生了变化,如果出现错误,就不能迅速定位到源文件。而source map就是为了解决这一问题:记录了bundle和源代码之间的转换映射关系。
而我们要开启sourceMap
的相关配置就需要配置devtool
项来根据个人需求来选择。
打包构建选项devtool
配置项请参考:https://www.webpackjs.com/configuration/devtool/。
模块热替换(热更新)HMR
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块, 而无需完全刷新。本页面重点介绍其 实现,而 概念 页面提供了更多关于 它的工作原理以及为什么它有用的细节。
具体请参考:模块热替换。
webpack打包工作模式
参考https://webpack.docschina.org/configuration/mode/
webpack的打包优化optimization
参考https://webpack.docschina.org/configuration/optimization/#root.