前端模块打包工具
需要解决的问题:
- 新特性代码编译
- 模块化Javascript打包
- 支持不同类型的资源模块
前端模块化工具
打包工具解决的是前端整体的模块化,并不单指JS模块化
- webpack
- 核心特性
- 模块加载器Loader
- 代码拆分 Code Splitting
- 资源模块 Asset Module
- 核心特性
- parcel
- rollup
webpack配置
webpack会自动地从src/index.js开始打包。
但是我们也可以通过配置,自定义入口文件 webpack.config.js
webpack工作模式
- 在打包指令中添加
mode
shell
yarn webpack --mode development
yarn webpack --mode production # 默认
yarn webpack --mode none // webpack以最原始的方式打包,不会做任何额外处理
-
在webpack配置文件中添加
mode
属性js// webpack.config.js module.exports = { mode: 'production', // 默认 // mode: 'development', // mode: 'none', }
webpack打包结果运行原理
webpack打包后的bundle.js,里面的代码是一个立即执行函数,入参为一个数组,数组每项表示一个模块函数。入口文件所对应的模块函数位于数组第一个,对应索引为0
。
js
(function(modules) {
var installModules = {};
var __webpack_require__ = (moduleId) {
// 判断是否是缓存的模块,是缓存模块就返回缓存模块
if(installModules[moduleId]) {
return installModules[moduleId].exports;
}
// 创建一个新模块,并把它放入缓存中
var module = (installModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});
// 执行模块函数
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// 标记模块已经加载
module.l = true;
// 返回模块的导出成员
return module.exports;
}
// 立即执行入口文件,并返回
return __webpack_require__(0);
})([
function(module, exports, require) {
var __webpack_import_foo_js__ = require(1);
__webpack_import_foo_js__['a']();
},
function(module, exports, require) {
exports['a'] = foo;
var foo = function() {
console.log('This is foo.js');
}
}
])
立即执行函数内部自定义实现了一个__webpack_require__
函数,因为函数内不支持ESM
,但支持以CJS
规范引入模块,所以自定义了__webpack_require__
函数以遵循CJS
规范,实现了CJS
的require
和module.exports
。
立即执行函数体内立即执行了下标为0
的入口模块函数:
js
// 立即执行入口文件,并返回
return __webpack_require__(0);
__webpack_require__
接收到参数moduleId
,内部自定了一个module
对象,存放着exports
属性,__webpack_require__
函数内部会调用执行modules[moduleId].call()
,也就是最外层数组索引对应的模块函数,返回值是module.exports
。
js
var __webpack_require__ = function(moduleId) {
var module = {
exports: {},
};
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__,
)
return module.exports;
}
来到modules[0]
这里:
js
function(module, exports, require) {
var __webpack_import_foo_js__ = require(1);
__webpack_import_foo_js__['a']();
},
里面调用了require(1)
,那也就是调用了__webpack_require__(1)
,即modules[1]
被执行:
js
function(module, exports, require) {
exports['a'] = foo;
var foo = function() {
console.log('This is foo.js');
}
}
回看modules[0]
里面,用__webpack_import_foo_js__
接收require(1)
的返回值(也就是__webpack_require__(1)
的返回值)module.exports
,而__webpack_require__(1)
被执行后,__webpack_require__(1)
的返回值module.exports
由
js
module: {
exports: {}
}
变成了
js
module: {
exports: {
'a': foo
},
}
而foo
被modules[0]
获取到并执行:
js
__webpack_import_foo_js__['a']();
这样入口模块的依赖模块就被成功获取并执行。其他依赖模块也一样的按照这个规律去被引入执行,这就是webpack打包文件的基本运行原理。
webpack样式资源模块加载
- css-loader 把css文件转成js
- style-loader 把css-loader的结果追加到页面上
webpack文件资源加载器
file-loader 如何工作
- webpack在打包过程中遇到了图片文件,然后根据配置文件中的配置,匹配到对应的文件加载器,此时文件加载器开始工作。
- 当前这个模块的返回值返回,对于应用来说,所需要的资源就被发布出来了。同时通过模块的导出成员拿到这个资源的访问路径
js
function (module, exports, __webpack_require__) {
module.exports =
__webpack_require__.p + "04cad6aa46cde16ca68207af1e03ade6.png";
},
__webpack_require__.p
表示的是webpack.config.js
配置文件里面配置的output.publicPath
DataURLs与url-loader
- dataURLs
- 是一种当前url就可以表示文件内容的方式,不会再发送任何的http请求
协议
媒体类型和编码
文件内容
data:[<mediatype>][;base64],<data>
- 这种方式适合体积比较小的资源,体积较大的话会造成打包结果体积大,从而影响运行速度
选file-loader还是url-loader
- 小文件使用
Data URLs
,减少请求次数 - 大文件单独提取存放,提高加载速度
- 对此,
url-loader
可以这样配置:
js
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024, // 10 KB
}
}
}
- 超过
10 KB
文件单独提取存放,这时还是需要安装file-loader
的,因为url-loader
会去调用它 - 小于
10 KB
文件转换为Data URLs
嵌入代码中
webpack与ES2015
- webpack默认就能处理
import
、export
,但是webpack未能自动编译js代码,webpack只是因为模块打包需要,所以处理了import
、export
,未能处理ES6
的其他新特性。 - 此时需要使用babel加载器
babel-loader
来编译js代码。 babel-loader
需要依赖babel其他的核心模块,所以需要安装下@babel/core
,以及用于去完成具体特性转换插件的集合@babel/preset-env
babel-loader
就会取代默认的加载器,在打包过程中就可以完成对新特性的编译转换。
js
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
]
webpack模块的加载方式
- 遵循
ES Modules
标准的import
声明 - 遵循
CommonJS
标准的require
函数 - 部分
Loader
加载的非 JavaScript 也会触发资源加载
- 例如样式代码中的
@import
指令和url
函数
css
@import url(reset.css);
body {
background: #f4f8fb;
min-height: 100vh;
background-image: url(icon.png);
background-size: cover;
}
- HTML 代码中
img
标签的src
属性、a
标签的href
属性
html
<img src="better.png" alt="better" width="236">
<a href="better.png"></a>
webpack核心工作原理
- 在项目中散落的代码及资源文件,webpack会根据我们的配置,找到其中的一个文件作为打包入口,一般情况下这个文件是一个JS文件。
- 然后顺着我们入口文件当中的代码,根据代码中出现的
import
或者require
之类的语句,解析推断出来这个文件所依赖的资源模块,然后分别去解析每个模块对应的依赖,最后形成了整个项目当中,所有用到的文件之间的一个依赖关系 - webpack会递归这个依赖树,然后找到每个节点对应的资源文件,最后根据我们配置文件当中的rules属性,找到这个模块对应的加载器,然后交给对应的加载器去加载这个模块
- 最后会将加载到的结果,放到
bundle.js
也就是我们的打包结果当中,从而实现整个项目的打包。
开发一个loader
loader专注于实现资源模块加载
js
const marked = require('marked');
module.exports = source => {
const html = marked(source);
// loader需要返回js语句
// return `module.exports = ${JSON.stringify(html)}`;
// 也可以使用ESM的导出方式
// return `export default ${JSON.stringify(html)}`
// 返回 html 字符串交给下一个 loader 处理
return html;
}
插件机制
Plugin专注于解决自动化工作
e.g.
- 清除dist目录
- 拷贝静态文件至输出目录
- 压缩输出代码
...
自动清除输出目录插件
clean-webpack-plugin
自动生成使用bundle.js的HTML
html-webpack-plugin
通过这个插件,自动生成dist目录中的index.html文件,包括对bundle.js的引入都是自动化的无需手动去修改。
默认生成的是index.html,如果有多个html文件,可以配置多个
js
plugins: [
new HtmlWebpackPlugin({ filename: 'xxx.html' }),
// 如果有多个html文件,可以配置多个 HtmlWebpackPlugin
new HtmlWebpackPlugin({ filename: 'xxx.html' })
]
直接拷贝项目根目录文件到输出文件夹
copy-webpack-plugin
像可以把整个根目录下public的资源完整copy到dist目录,那么就可以:
js
new CopyWebpackPlugin([
'public'
])
开发一个 webpack 插件
- plugin 通过钩子机制实现
- 插件必须是
一个函数
或者是一个包含 apply 方法的对象
- 插件通过在生命周期的钩子中挂载函数实现扩展
js
// 实现一个插件,去除打包文件中的内容开头的/****/
class MyPlugin {
apply(compiler) {
console.log('MyPlugin启动');
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
// compilation.assets
for(const name in compilation.assets) {
if(name.endsWith('.js')) {
const contents = compilation.assets[name].source();
const withoutComments = contents.replace(/\/\*\*+\//g, '');
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length,
}
}
}
})
}
}
webpack开发体验问题
需要解决的问题:
- 以http Server 运行,更接近生产环境、可以请求ajax数据
- 自动编译、自动刷新,大大减少开发过程中重复的操作
- 提供 Source Map 支持,运行过程中一旦出现错误,就可以根据错误的推断信息快速定位到源代码当中的对应位置,便于调试应用
自动编译
- watch 工作模式
shell
yarn webpack --watch
监听文件变化,自动重新打包
- 希望编译过后自动刷新浏览器
webpack开发工具-webpack-dev-server
-
提供用于开发的 HTTP Server,集成
自动编译
、自动刷新浏览器等功能
-
webpack-dev-server为了提高工作效率,所以并没有将打包结果写入到磁盘中, 而是将打包结果暂存到内存当中,而内部的http-server会从内存当中把文件读取出来发送给浏览器,这样一来会将少很多不必要的读写操作,从而大大提高构建速率。
-
--oepn参数可以自动唤起浏览器,打开运行地址
-
默认会将构建结果输出的文件,全部作为开发服务器的文件,也就是说,只要是webpack输出的文件,都可以直接被访问。如果有其他静态资源文件也需要serve,就需要额外的告诉webpack-dev-server,所以需要在配置文件中配置下:
jsdevServer: { contentBase: './public' // 多个用数组。这样就可以在浏览器中访问 public 下的文件了,e.g. http://localhost:8080/better.png },
-
代理API服务。本地代码运行在
http://localhost:8080
上面,而最终上线过后应用和API会部署到同源地址上面。那这样就会有一个非常常见的问题,在实际生产环境中,我们可以直接去访问API,但是开发环境当中就会产生跨域请求问题,并不是所有API都应该支持CORS(跨域资源共享), 如果是前后端同源部署的话,不需要开启CORS,所以就会导致开发阶段接口的跨域问题。最好的办法就是在开发服务器当中配置代理服务,也就是把API接口服务代理到本地的这个开发服务器地址jsproxy: { // 每一个属性是一个代理规则的配置 // key-需要被代理的请求路径前缀,值-为这个前缀所匹配到的代理规则配置 '/api': { // target-代理目标 // http://localhost:8080/api/users -> https://api.github.com/api/users target: 'https://api.github.com', // 重写规则 // https://api.github.com/api/users -> https://api.github.com/users pathRewrite: { '^/api': '', }, // true-不能使用localhost:8080作为GitHub的主机名,因为 localhost:8080对于GitHub来说是不认识的 changeOrigin: true } }
changOrigin
设置为true,这是因为默认代理服务器会以我们实际在浏览器当中请求的主机名,也就是localhost:8080作为代理请求的主机名,也就是我们在浏览器端对我们代理过后的这个地址发起请求,那这个请求背后肯定还需要请求到GitHub的服务器,请求的过程当中会带一个主机名,那这个主机名默认情况下使用的是我们用户在浏览器端发起请求的这个主机名,也就是localhost:8080,而一般情况下服务器那边需要根据主机名去判断,这个请求是属于哪个网站,从而把这个请求指派到对应的网站。localhost:8080对GitHub服务器来说肯定是不认识的,所以这里需要修改。changOrigin等于true的情况下,就会以实际我们代理请求这次发生的过程中的主机名去请求。
SourceMap
- 以
.map
为文件名结尾 SourceMap
翻译过来就是源代码地图,指的是源代码和运行代码之间的映射关系,解决了源代码与运行代码不一致所产生的问题- 例如在文件
jquery-3.4.1.min.js
结尾添加:// #sourceMappingURL=jquery-3.4.1.min.map
,打开浏览器调试的时候,就可以看源代码
webpack配置SourceMap
js
devtool: 'source-map',
- webpack有多种SourceMap的配置方式。
- 每种方式的效率和效果各不相同,效果好的效率低,效果差的效率高
不同 devtool 之间的差异
- eval
- 把模块代码放到
eval
函数中执行,并通过sourceUrl
标注模块文件的路径 - 没有生成对应的
SourceMap
,只能定位是哪个文件
出了错误
- eval-source-map
- 同样也是用
eval
函数执行模块代码 - 不仅可以定位模块
文件路径
,还可以定位到出错的行和列
- 相比于eval,
生成了SourceMap
- cheap-eval-source-map
- 与eval-source-map一样,不同的是
不能定位出错位置的 列 信息
- 生成速度比较快
- cheap-module-eval-source-map
- 与eval-source-map一样, 不同的是模块代码
没有经过es6转换
- inline-source-map
-
跟普通的source-map效果一样,
-
不同的是,普通的source-map是以物理文件
bundle.js
的形式存在jsbundle.js bundle.js.map // 在bundle.js结尾添加 sourceMappingURL=bundle.js.map
-
而inline-source-map的源代码以
base64的形式内嵌在map文件中存在
jsbundle.js.map // 在bundle.js结尾添加 sourceMapUR=data:application/json;xxxx
这种方式一般不会被使用,因为把SourceMap放到源代码过后体积会变大
- hidden-source-map
- 在浏览器开发工具中看不到SourceMap的效果,但是SourceMap文件确实有生成
- 这是因为在代码当中没有引入这个SourceMap文件(也就是没有在代码结尾添加sourceMappingURL=xxx)
- 这种方式在开发一些第三方包的时候会比较有用,比如 jquery
- nosources-source-map
- 能看到错误出现的行列位置信息,但是不能看到源代码
- 这种是为了在生产环境中保护源代码不被暴露
SourceMap的选择
-
开发环境:cheap-module-eval-source-map 原因:
- 代码每行不会超过80个字符
- 代码经过Loader转换过后的差异较大
- 虽然首次打包速度慢,但是重写打包相对较快
-
生产环境: none/nosource-source-map 原因:
- SourceMap会暴露源代码
- 调试是开发阶段的事情
-
记忆技巧:
- eval 是否使用eval执行模块代码、只定位出错文件路径
- cheap 没有列信息
- module 不做es语法编译转换
自动刷新HMR
- 模块热替换,指的是应用运行过程中实时替换某个模块,应用运行状态不受影响。
- 如果没有模块热替换,自动刷新会导致整个页面状态的丢失
- 热替换只将修改的模块实时替换至应用中
如何使用HMR
- HMR集成在 webpack-dev-server中
- 开启方式:
- 在运行
webpack-dev-server
这个命令时加--hot
参数 - 配置文件开启
js
devServer: {
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
- webpack的HMR并不可以开箱即用。webpack中的HMR需要手动处理模块热替换逻辑
- 之所以样式文件可以实现热替换,是因为样式使用的了loader,样式改变后webpack可以及时地识别替换,而js的导出内容无规律,webpack很难去判断识别。
- 框架的脚手架可以实现js的热替换,是因为框架本身就是有规律可循的,而不是非框架的js那样无规律,这样实现HMR会较为容易
HMR API
js
// main.js
module.hot.accept('./heading.js', () => {
console.log('heading 模块更新了,需要这里手动处理热替换逻辑');
});
HMR 需要注意的坑
- 处理HMR的代码报错会导致自动刷新,报错信息会被清除,导致无法知道哪里出错。解决,使用
hotOnly
js
devServer: {
hotOnly: true,
}
- 没启用 HMR 的情况下,HMR API 报错
js
// 使用的时候,做一个判断是否存在
if(module.hot) {
...
}
- 使用 HMR API,代码中是否多了一些与业务无关的代码? 在生产环境中不会存在 HMR API 相关的代码
js
// 在 yan build 之后的代码:
// 在生产环境中,代码压缩之后这个条件也不会被打包到最终文件中
if(false) {}
生产环境优化
- 开发环境侧重提高开发体验,提供了SourceMap调试代码、HMR热替换模块功能等功能,自动往打包结果中添加了一些额外代码。这些代码对于生产环境来说是冗余的。
- 生产环境更注重运行效率,开发环境更注重开发效率
- 为了解决这些问题,webpack4推出了mode用法,为不同模式提供一些预设配置
- 同时webpack也建议我们为不同的工作环境创建不同的配置
不同环境下的配置
- 配置文件根据环境的不同导出不同配置
js
// webpack 配置文件还支持导出一个函数,这个函数接收两个参数,返回我们需要的配置对象
// env - 通过cli传递的环境参数
// argv - 运行cli过程中所传递的所有参数
module.exports = (env, argv) => {
const config = {
...
}
if(env === 'production') {
config.mode = 'production';
config.devtool = false;
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
// 开发阶段最好不要使用这个插件,用 devServer的 contentBase
new CopyWebpackPlugin([
'public'
]),
]
}
return config;
}
-
一个环境对应一个配置文件
webpack.common.js
webpack.prod.js
webpack.dev.js
js
// webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const common = require('./webpack.common.js');
const merge = require('webpack-merge');
module.exports = merge({}, common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public']),
]
});
- 打包命令
shell
# 指明使用哪个配置文件打包
yarn webpack --config webpack.prod.js
- 也可以修改打包命令:
json
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server"
}
DefinePlugin
- 为代码注入全局成员
- 在production模式下,默认这个插件会启用,往代码中注入了
process.env.NODE_ENV
常量,很多第三方工具都是通过这个常量去判断当前环境从而决定是否执行某些任务
js
// webpack.config.js
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
},
plugins: [
new webpack.DefinePlugin([
API_BASE_URL: JSON.stringify('"https://api.example.com"')
])
]
}
TreeShaking
- 摇掉未引用的代码
- 在生产模式下会自动开启
- 不是某个配置选项,而是一组功能搭配使用后的优化效果
TreeShaking 的使用
js
// 集中配置webpack内部的优化功能
optimization: {
useExports: true, // 只导出外部使用到的成员
minimize: true, // 开启代码压缩功能
},
webpack合并模块
-
除了
useExports
以外,还可以使用concatenateModules
继续优化输出 -
普通的打包结果是把每个模块放到单独的一个函数当中,如果模块很多,就会有很多这样的模块函数
js// 集中配置webpack内部的优化功能 optimization: { // useExports: true, // 只导出外部使用到的成员 // minimize: true, // 开启代码压缩功能 concatenateModules: true, // 尽可能合并每一个模块到一个函数中 },
-
concatenateModules
作用是尽可能将所有模块合并输出到一个函数中,提升运行效率,减少代码体积 -
这个特性又被称之为
Scope Hoisting
,即作用域提升,这是webpack3添加的一个特性,配合minimize使用,代码体积又会进一步减少
Tree Shaking 与 Babel
-
Tree Shaking 的前提是使用
ESM
来组织代码,也就是由 webpack 打包的代码必须使用ESM
-
为了转换代码中的ESM新特性,会使用到
babel-loader
,有可能会把ESM
转成CJS
,这取决于有没有使用到ESM转为CJS的插件,比如@babel/preset-env
,那么这时 Tree Shaking会失效,但是,在最新的babel-loader中自动关闭了ESM
转为CJS
的插件,所以 Tree Shaking 还是有效的 -
如果要强行转成
CJS
,也是可以通过配置实现的:jsmodule: { rules: { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { modules: 'commonjs' }] ] } } } }
webpack 与 sideEffects
- sideEffects,即副作用
- 通过配置的方式,去标识我们的代码是否有副作用,从而为 Tree Shaking 提供更大的压缩空间
- 副作用是指模块执行时,除了导出成员之外所作的事情
- sideEffects一般在NPM包中用到,用来标记是否有副作用
- 开启副作用,webpack打包时会先检查当前代码所属的package.json当中有没有sideEffects这样一个标识,以此来判断这个模块是否有副作用
- 如果这个模块没有副作用,
"sideEffects": false
,那这些没有副作用的模块就不会被打包
- 生产环境中,webpack 默认会开启 sideEffects
- sideEffects涉及到两个地方,
webpack.config.js
中的sideEffects: true
表示开启这个功能,package.json
中的sideEffects: false
标识有没有副作用
json
// package.json
{
"sideEffects": false
}
js
// webpack.config.js
optimization: {
sideEffects: true
}
sideEffects使用注意
- 使用前提是,确保你的代码真的没有副作用,否则webpack会误删掉有副作用的代码
- 样式文件
.css
也是会有副作用的 - 为了标识有副作用的文件,可以这样:
json
// package.json
{
"sideEffects": [
"./src/extend.js",
"*.css"
]
}
- 那么除了这些有副作用的,其他没有副作用的文件将会被webpack做sideEffects处理
Code Splitting
- 分包/代码分割
- 如果没有代码分割,所有代码都会被打包到一起,bundle体积过大
- 实际上,在应用开始工作时,并不是每个模块在启动的时候都是必要的,但是所有的模块都打包到一起,那么我们需要任何一个模块的时候都必须把整体加载下来过后才能使用,而我们的应用一般都运行浏览器端,就意味着我们会浪费掉很多的流量和带宽
- 更为合理的方案,就是把我们的打包结果按一定的规则分离到多个bundle中,然后根据应用的运行需要,按需加载这些模块,也就是分包
- 这样就可以大大提高应用的响应速度以及运行效率
分包方式
- 多入口打包,输出多个打包结果
-
适用于传统的多页应用程序
-
一个页面对应一个打包入口
-
公共部分单独提取
-
配置,
entry
是一个对象(注意不是数组,数组的话会把多个页面打包到一起)output
输出文件名filename
- 输出html的插件------
HtmlWebpackPlugin
会输出一个自动注入所有打包结果的html文件, 但是这里需要指定输出的html文件所使用的bundle,就需要用到chunks
属性,这样每个打包入口就会形成一个独立的chunk
jsentry: { // key - 入口的名称,值 - 入口所对应的文件路径 index: './src/index.js', about: './src/about.js' }, output: { filename: '[name].bundle.js' }, plugins: [ new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', meta: { viewport: 'width=device-width' }, template: './src/index.html', chunks: ['index'] }), // 用于生成 about.html new HtmlWebpackPlugin({ filename: 'about.html', chunks: ['about'] }), ]
-
提取公共模块 Split Chunks
- 多入口文件会存在一个问题,就是不同入口中肯定会有公共模块
- 所以需要将公共模块提取到单独的chunk中
- webpack实现公共模块提取的方式,通过Split Chunks实现:
jsoptimization: { splitChunks: { chunks: 'all' // all 表示把所有的公共模块都提取到一个单独的chunk中 } }
- 动态导入,
ESM
实现模块按需加载,webpack会把按需加载的模块单独输出到一个bundle当中
-
按需加载,指的是需要用到某个模块的时候,再加载这个模块
-
所有动态导入的模块会被自动分包
-
webpack内部会自动处理分包和按需加载,无需我们手动配置
js// 静态导入 // 打包入口文件index.js import posts from './posts/posts' import album from './album/album' const render = () => { const hash = window.location.hash || '#posts'; const mainElement = document.querySelector('.main'); mainElement.innerHTML = ''; if(hash === '#posts') { mainElement.appendChild(posts()); } else if(hash === '#alnum') { mainElement.appendChild(album()); } } render(); window.addEventListener('hashchange', render);
js// 动态导入 // 打包入口文件index.js const render = () => { const hash = window.location.hash || '#posts'; const mainElement = document.querySelector('.main'); mainElement.innerHTML = ''; if(hash === '#posts') { // import函数实现动态导入 import('./posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()); }); } else if(hash === '#alnum') { // import函数实现动态导入 import('./album/album').then(({ default: album }) => { mainElement.appendChild(album()); }) } } render(); window.addEventListener('hashchange', render);
魔法注释 Magic Comments
-
默认按需加载产生的bundle文件,名称只是一个序号,如果需要给这些bundle命名的话,可以使用魔法注释
jsimport(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()); });
MiniCssExtractPlugin
-
提取CSS文件
-
如果样式不是很多(少于150kb),不建议用,不然效果反而适得其反
-
MiniCssExtractPlugin
会自动提取代码当中的CSS到一个单独的css文件当中 -
所以不再需要
style-loader
将样式通过style标签注入,而是替换为使用MiniCssExtractPlugin.loader
通过link
的方式引入 -
配置:
jsmodule.exports = { module: { rules: [ { test: /.css$/, use: [ // 'style-loader', // 把样式通过 style 标签注入,使用 MiniCssExtractPlugin 就不用这个 MiniCssExtractPlugin.loader, // 使用 MiniCssExtractPlugin 就用这个,把样式 link 到 html 'css-loader' ] } ] }, plugins: [ // MiniCssExtractPlugin会自动提取代码当中的CSS到一个单独的文件当中 // 所以不再需要style-loader将样式通过style标签注入 // 而是替换为使用MiniCssExtractPlugin.loader通过link的方式引入 new MiniCssExtractPlugin(), // 如果样式不是很多(少于150kb),不建议用,不然效果反而适得其反 ] }
OptimizeCssAssetsWebpackPlugin
-
压缩 css
-
在生产环境,不配置任何压缩插件时,默认会压缩js,但是css文件不会被压缩
-
在 plugins 中配置压缩css压缩插件
OptimizeCssAssetsWebpackPlugin
,表示在任何情况下都有效,包括开发环境 -
optimization 的
minimizer
配置 css 压缩插件会覆盖原有的默认值,导致 js 的压缩功能会丢失,所以需要安装压缩js的插件TerserWebpackPlugin
来对js进行压缩js// 在 plugins 中配置压缩插件,表示在任何情况下都有效 plugins: [ new OptimizeCssAssetsWebpackPlugin(), // 压缩打包后的css new TerserWebpackPlugin(), ] // 或者在 optimization 的 `minimizer` 属性中按需配置需要用到的压缩插件 // 注意这里配置后会覆盖原有的默认值,所以除了配置 css 压缩插件,还需要补充 js 压缩插件,不然 js 的压缩功能会丢失 optimization: { minimizer: [ new OptimizeCssAssetsWebpackPlugin(), ] }
输出文件名 hash
-
部署前端项目时,都会启用服务器的静态资源缓存,这样对于用户的浏览器而言,就可以缓存前端的静态资源,后续就不再需要请求服务器得到这些静态资源,这样整体应用的响应速度就可以得到提升
-
不过开启静态资源的客户端缓存,也会有一些小问题。如果在缓存策略当中,缓存失效时间设置过短,效果不是特别明显,缓存失效时间过长,一旦应用发生了更新,重新部署过后,又没有办法及时更新到客户端
-
为了解决这个问题,在生产环境下,需要给输出的文件名添加
hash
值。这样一旦资源文件发生改变,文件名称也可以跟着变。对于客户端而言,全新的文件名就是全新的请求,也就没有缓存的问题,这样也就可以把服务端的缓存策略当中的时间设置得比较长,也就不用担心文件更新过后的问题 -
webpack配置filename支持的hash
- [hash],整个项目级别,也就是项目当中有任何一个地方改动,那么这次打包过程中的所有hash值都会发生变化
- [chunkhash],chunk级别,也就是在打包过程中,只要是同一路的打包,chunkhash都会发生变化
- [contenthash],文件级别,根据输出文件的内容生成的hash值,也就是不同的文件就有不同的hash值,只有文件发生了变化,才会更新hash
jsoutput: { // 输出文件的名称 // filename: '[name]-[hash]bundle.js', // filename: '[name]-[chunkhash]bundle.js', filename: '[name]-[contenthash]bundle.js', },
-
指定hash长度
-
e.g.
[contenthash:8]
js
output: {
// 输出文件的名称
// filename: '[name]-[hash]bundle.js',
// filename: '[name]-[chunkhash]bundle.js',
filename: '[name]-[contenthash:8]bundle.js',
},
以上是关于webpack4的一些核心配置知识,后面有时间会写一篇webpack5的。感谢大家用心的阅读。
参考
[1] 「前端工程化」之 Webpack 原理与实践(彻底搞懂吃透 Webpack)汪磊原版-b站