当前依赖包的版本
1.优化loader配置
1.1 缩小文件匹配范围(include/exclude)
通过排除node_modules下的文件 从而缩小了loader加载搜索范围 高概率命中文件
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, 'src') // 精确指定要处理的目录
}
]
}
1.2 缓存loader的执行结果(cacheDirectory)
cacheDirectory是loader的一个特定的选项,默认值是false。指定的目录(use: 'babel-loader?cacheDirectory=cacheLoader')将用来缓存loader的执行结果,减少webpack构建时Babel重新编译过程。如果设置一个空值(use: 'babel-loader?cacheDirectory') 或true(use: 'babel-loader?cacheDirectory=true') 将使用默认的缓存目录(node_modules/.cache/babel-loader),如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader?cacheDirectory', // 缓存loader执行结果 发现打包速度已经明显提升了
exclude: /node_modules/,
include: path.resolve(__dirname, 'src')
}
]
}
2.resolve优化配置
2.1 优化模块查找路径 resolve.modules
Webpack的resolve.modules配置模块库(即 node_modules)所在的位置,在 js 里出现 import 'vue' 这样不是相对、也不是绝对路径的写法时,会去 node_modules 目录下找。但是默认的配置,会采用向上递归搜索的方式去寻找,但通常项目目录里只有一个 node_modules,且是在项目根目录,为了减少搜索范围,可以直接写明 node_modules 的全路径;同样,对于别名(alias)的配置,亦当如此:
const path = require('path');
function resolve(dir) { // 转换为绝对路径
return path.join(__dirname, dir);
}
resolve: {
modules: [ // 优化模块查找路径
path.resolve('src'),
path.resolve('node_modules') // 指定node_modules所在位置 当你import 第三方模块时 直接从这个路径下搜索寻找
]
}
配置好src目录所在位置后,由于util目录是在src里面 所以可以用下面方式引入util中的工具函数
// main.js
import dep1 from 'util/dep1';
import add from 'util/add';
2.2 resolve.alias 配置路径别名
创建 import 或 require 的路径别名,来确保模块引入变得更简单。配置项通过别名来把原导入路径映射成一个新的导入路径 此优化方法会影响使用Tree-Shaking去除无效代码
例如,一些位于 src/ 文件夹下的常用模块:
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
现在,替换「在导入时使用相对路径」这种方式,就像这样:
import Utility from '../../utilities/utility';
你可以这样使用别名:
import Utility from 'Utilities/utility';
resolve: {
alias: { // 别名配置 通过别名配置 可以让我们引用变的简单
'vue$': 'vue/dist/vue.common.js', // $表示精确匹配
src: resolve('src') // 当你在任何需要导入src下面的文件时可以 import moduleA from 'src/moduleA' src会被替换为resolve('src') 返回的绝对路径 而不需要相对路径形式导入
}
}
也可以在给定对象的键后的末尾添加 $,以表示精准匹配:
alias: {
util$: resolve('src/util/add.js')
}
这将产生以下结果:
import Test1 from 'util'; // 精确匹配,所以 src/util/add.js 被解析和导入
import Test2 from 'util/dep1.js'; // 精确匹配,触发普通解析 util/dep1.js
2.3resolve.extensions
当引入模块时不带文件后缀 webpack会根据此配置自动解析确定的文件后缀
- 后缀列表尽可能小
- 频率最高的往前放
- 导出语句尽可能带上后缀
resolve: {
extensions: ['.js', '.vue']
}
3.module.noParse
用了noParse的模块将不会被loaders解析,所以当我们使用的库如果太大,并且其中不包含import require、define的调用,我们就可以使用这项配置来提升性能, 让 Webpack 忽略对部分没采用模块化的文件的递归解析处理。
// 忽略对jquery lodash的进行递归解析
module: {
// noParse: /jquery|lodash/
// 从 webpack 3.0.0 开始
noParse: function(content) {
return /jquery|lodash/.test(content)
}
}
4.HappyPack
HappyPack是让webpack对loader的执行过程,从单一进程形式扩展为多进程模式,也就是将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。从而加速代码构建 与 DLL动态链接库结合来使用更佳。
npm i happypack@next -D
webpack.config.js
const HappyPack = require('happypack');
const os = require('os'); // node 提供的系统操作模块
// 根据我的系统的内核数量 指定线程池个数 也可以其他数量
const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().lenght})
module: {
rules: [
{
test: /\.js$/,
use: 'happypack/loader?id=babel',
exclude: /node_modules/,
include: path.resolve(__dirname, 'src')
}
]
},
plugins: [
new HappyPack({ // 基础参数设置
id: 'babel', // 上面loader?后面指定的id
loaders: ['babel-loader?cacheDirectory'], // 实际匹配处理的loader
threadPool: happyThreadPool,
// cache: true // 已被弃用
verbose: true
});
]
happypack提供的loader,是对文件实际匹配的处理loader。这里happypack提供的loader与plugin的衔接匹配,则是通过id=happypack来完成。 资料:happypack原理解析-淘宝前端
npm run dev
5.DLL动态链接库
在一个动态链接库中可以包含其他模块调用的函数和数据,动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会被重新编译,而是直接使用动态链接库中的代码。
- 将web应用依赖的基础模块抽离出来,打包到单独的动态链接库中。一个链接库可以包含多个模块。
- 当需要导入的模块存在于动态链接库,模块不会再次打包,而是去动态链接库中去获取。
- 页面依赖的所有动态链接库都需要被加载。
5.1 定义DLL配置
依赖的两个内置插件:DllPlugin 和 DllReferencePlugin
5.1.1 创建一个DLL配置文件webpack_dll.config.js
module.exports = {
entry: {
react: ['react', 'react-dom']
},
output: {
filename: '[name].dll.js', // 动态链接库输出的文件名称
path: path.join(__dirname, 'dist'), // 动态链接库输出路径
libraryTarget: 'var', // 链接库(react.dll.js)输出方式 默认'var'形式赋给变量 b
library: '_dll_[name]_[hash]' // 全局变量名称 导出库将被以var的形式赋给这个全局变量 通过这个变量获取到里面模块
},
plugins: [
new webpack.DllPlugin({
// path 指定manifest文件的输出路径
path: path.join(__dirname, 'dist', '[name].manifest.json'),
name: '_dll_[name]_[hash]', // 和library 一致,输出的manifest.json中的name值
})
]
}
5.1.2 output.libraryTarget 规定了以哪一种导出你的库 默认以全局变量形式 浏览器支持的形式
具体包括如下:
-
"var" - 以直接变量输出(默认library方式) var Library = xxx (default)
-
"this" - 通过设置this的属性输出 this["Library"] = xxx
-
"commonjs" - 通过设置exports的属性输出 exports["Library"] = xxx
-
"commonjs2" - 通过设置module.exports的属性输出 module.exports = xxx
-
"amd" - 以amd方式输出
-
"umd" - 结合commonjs2/amd/root
5.1.3 打包生成动态链接库
webpack --config webpack_dll.config.js --mode production
在dist目录下 多出react.dll.js 和 react.manifest.json
- react.dll.js 动态链接库 里面包含了 react和react-dom的内容
- react.manifest.json 描述链接库(react.dll)中的信息
5.2 在主配置文件中使用动态链接库文件
// webpack.config.js
const webpack = require('webpack');
plugins: [
// 当我们需要使用动态链接库时 首先会找到manifest文件 得到name值记录的全局变量名称 然后找到动态链接库文件 进行加载
new webpack.DllReferencePlugin({
manifest: require('./dist/react.manifest.json')
})
]
5.3 将动态链接库文件加载到页面中
需要借助两个webpack插件
- html-webpack-plugin 产出html文件
- html-webpack-include-assets-plugin 将js css资源添加到html中 扩展html插件的功能
npm i html-webpack-plugin html-webpack-include-assets-plugin -D
配置webpack.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlIncludeAssetsPlugin = require('html-webpack-include-assets-plugin');
pluings: [
new webpack.DllReferencePlugin({
manifest: require('./dist/react.manifest.json')
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html')
}),
new HtmlIncludeAssetsPlugin({
assets: ['./react.dll.js'], // 添加的资源相对html的路径
append: false // false 在其他资源的之前添加 true 在其他资源之后添加
});
]
此时react.dll.js和main.js被自动引入到页面中,并且dll文件在main.js之前加载
6.ParallelUglifyPlugin
这个插件可以帮助有很多入口点的项目加快构建速度。把对JS文件的串行压缩变为开启多个子进程并行进行uglify。
cnpm i webpack-parallel-uglify-plugin -D
// webpck.config.js
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
plugins: [
new ParallelUglifyPlugin({
workerCount: 4,
uglifyJS: {
output: {
beautify: false, // 不需要格式化
comments: false // 保留注释
},
compress: { // 压缩
warnings: false, // 删除无用代码时不输出警告
drop_console: true, // 删除console语句
collapse_vars: true, // 内嵌定义了但是只有用到一次的变量
reduce_vars: true // 提取出出现多次但是没有定义成变量去引用的静态值
}
}
});
]
执行压缩
webpack --mode production
7.Tree Shaking
剔除JavaScript中用不上的代码。它依赖静态的ES6模块化语法,例如通过impot和export导入导出
commonJS模块 与 es6模块的区别
commonJS模块:
-
1.动态加载模块 commonJS 是运行时加载 能够轻松实现懒加载,优化用户体验
-
2.加载整个模块 commonJS模块中,导出的是整个模块
-
3.每个模块皆为对象 commonJS模块被视作一个对象
-
4.值拷贝 commonJS的模块输出和函数的值传递相似,都是值得拷贝
es6模块
-
1.静态解析 es6模块时 编译时加载 即在解析阶段就确定输出的模块的依赖关系,所以es6模块的import一般写在被引入文件的开头
-
2.模块不是对象 在es6里,每个模块并不会当做一个对象看待
-
3.加载的不是整个模块 在es6模块中 一个模块中有好几个export导出
-
4.模块的引用 es6模块中,导出的并不是模块的值得拷贝,而是这个模块的引用
7.1 保留ES6模块化语法
// .babelrc
{
"presets": [
[
"env", {
modules: false // 不要编译ES6模块
},
"react",
"stage-0"
]
]
}
7.2 执行生产编译 默认已开启Tree Shaking
webpack --mode production
什么是Tree Shaking?
有个funs.js 里面有两个函数
// funs.js
export const sub = () => 'hello webpack!';
export const mul = () => 'hello shaking!';
main.js 中依赖funs.js
// main.js
import {sub} from './funs.js'
sub();
在main.js只使用了里面的 sub函数 默认情况下也会将funs.js里面其他没有的函数也打包进来, 如果开启tree shaking 生产编译时
webpack --mode production //此时funs.js中没有被用到的代码并没打包进来 而被剔除出去了
区分环境
未完待续...
提取公共代
未完待续...
代码分离
未完待续...
开启Scope Hoisting
未完待续...
一 缩小文件搜索范围
1 include & exclude
1) action
2) useage
module: { rules: [ { test: /\.js$/, use: ['babel-loader?cacheDirectory'], include: path.resolve(__dirname, 'src'), exclude: /node_modules/ } ] }
You can also speed up babel-loader by as much as 2x by using the cacheDirectory option. This will cache transformations to the filesystem.
QA
命令行warning
加上exclude限制范围就不会报错了
2 resolve.modules
1) action
2) useage
resolve: { modules: [path.resolve('node_modules'), path.resolve('lib')] }
3) note
Absolute and relative paths can both be used, but be aware that they will behave a bit differently.
A relative path will be scanned similarly to how Node scans for node_modules, by looking through the current directory as well as it's ancestors (i.e. ./node_modules, ../node_modules, and on).
With an absolute path, it will only search in the given directory.
If you want to add a directory to search in that takes precedence over node_modules/:(即是有先后顺序的)
modules: [path.resolve(__dirname, "src"), "node_modules"]
4) QA
当你需要指定除node_modules之外的其它模块目录的时候可以在数组中添加属性
3 resolve.mainFields
1) action
安装的第三方模块中都会有一个 package.json文件,用于描述这个模块的属性,其中有些字段用于描述入口文件在哪里,resolve.mainFields 用于配置采用哪个字段作为入口文件的描述。
可以存在多个字段描述入口文件的原因是因为有些模块可以同时用在多个环境中,针对不同的运行环境需要使用不同的代码。 以 isomorphic-fetch API 为例,它是 Promise的一个实现,但可同时用于浏览器和 Node.js 环境。
2) useage
为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用 main字段去描述入口文件的位置,可以这样配置 Webpack:
module.exports = { resolve: { // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤 mainFields: ['main'], }, };
4 resolve.alias
1) action
2) useage
5 resolve.extensions
1) action
2) useage
3) note
6 module.noParse
1) action
2) useage
2) note
二 DLL
1 action
dll 为后缀的文件称为动态链接库,在一个动态链接库中可以包含给其他模块调用的函数和数据
2 usage
本次例子用jquery举例
1) 定义DLL
module.exports = { entry: ["jquery"], output: { filename: "vendor.js", path: path.resolve(__dirname, "dist"), libraryTarget: 'var',// 打包的方式,hou library: "vendor_lib_vendor"// DLL的名字 }, plugins: [ new webpack.DllPlugin({ name: "vendor_lib_vendor",// 定义DLL path: path.resolve(__dirname, "dist/vendor-manifest.json") }) ] };
"dll": "webpack --config webpack.jquery.config.js --mode development"
配置好上述的文件后,在终端运行
npm run dll
,时候会在dist目录下生成两个文件,分别是vendor.js
和vendor-manifest.json
。vendor.js
包含的就是打包后的jquery
文件代码,vendor-manifest.json
是用来做关联的。DLL定义好了,接下来就是应用打包好的DLL了。2) 引用DLL
plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require("./dist/vendor-manifest.json") }) ],
3) 使用DLL
<script src="./vendor.js"></script>
3 note
当用 Webpack 去构建一个可以被其他模块导入使用的库时需要用到它们。
output.libraryTarget 是字符串的枚举类型,支持以下配置。
1) var (默认)
编写的库将通过 var 被赋值给通过 library 指定名称的变量。
假如配置了 output.library='LibraryName',则输出和使用的代码如下:
2) commonjs2
编写的库将通过 CommonJS 规范导出。
假如配置了 output.library='LibraryName',则输出和使用的代码如下:
// Webpack 输出的代码 exports['LibraryName'] = lib_code; // 使用库的方法 require('library-name-in-npm')['LibraryName'].doSomething(); // 其中 library-name-in-npm 是指模块发布到 Npm 代码仓库时的名称。
3) commonjs2
编写的库将通过 CommonJS2 规范导出,输出和使用的代码如下:
// Webpack 输出的代码 module.exports = lib_code; // 使用库的方法 require('library-name-in-npm').doSomething();
CommonJS2 和 CommonJS 规范很相似,差别在于 CommonJS 只能用 exports 导出,而 CommonJS2 在 CommonJS 的基础上增加了 module.exports 的导出方式。 在 output.libraryTarget 为 commonjs2 时,配置 output.library 将没有意义。
4) this
编写的库将通过 this 被赋值给通过 library 指定的名称,输出和使用的代码如下:
// Webpack 输出的代码 this['LibraryName'] = lib_code; // 使用库的方法 this.LibraryName.doSomething();
5) window
编写的库将通过 window 被赋值给通过 library 指定的名称,即把库挂载到 window 上,输出和使用的代码如下:
// Webpack 输出的代码 window['LibraryName'] = lib_code; // 使用库的方法 window.LibraryName.doSomething();
6) global
编写的库将通过 global 被赋值给通过 library 指定的名称,即把库挂载到 global 上,输出和使用的代码如下:
// Webpack 输出的代码 global['LibraryName'] = lib_code; // 使用库的方法 global.LibraryName.doSomething();
三 HappyPack
1 action
HappyPack就能让Webpack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
2 usage
由于webpack 4.0 刚刚发布,响应的插件还没有更新完,不过可以在后面加一个
@next
来安装即将发布的版本module: { rules: [ { test: /\.css$/, use: 'happypack/loader?id=css', //把对.js文件的处理转交给id为babel的HappyPack实例 //用唯一的标识符id来代表当前的HappyPack是用来处理一类特定文件 include: path.resolve('./src'), exclude: /node_modules/ }, { test: /\.js/, use: 'happypack/loader?id=babel', include: path.resolve('./src'), exclude: /node_modules/ } ] }, plugins: [ new HtmlWebPackPlugin({ template: './src/index.html' }), new HappyPack({ id: 'babel', loaders: ['babel-loader']// 和rules里的配置相同 }), new HappyPack({ id: 'css', loaders: ['style-loader', 'css-loader']// 和rules里的配置相同 }), ]
四 ParallelUglifyPlugin
1.action
2 usage
new ParallelUglifyPlugin({ workerCount: os.cpus().length - 1,//开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1 uglifyJS: { output: { beautify: false, //不需要格式化 comments: true, //不保留注释 }, compress: { warnings: false, // 在UglifyJs删除没有用到的代码时不输出警告 drop_console: true, // 删除所有的 `console` 语句,可以兼容ie浏览器 collapse_vars: true, // 内嵌定义了但是只用到一次的变量 reduce_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 } } })
五 服务器自动刷新
1 文件监听
1) action
2) useage
watch: true, watchOptions: { ignored: /node_modules/, aggregateTimeout: 300, poll: 1 }
3) note
只有在开启监听模式时(watch为true),watchOptions才有意义
监听到变化发生后等300(ms)再去执行动作,防止文件更新太快导致编译频率太高
通过不停的询问文件是否改变来判断文件是否发生变化,默认每秒询问1000次
webpack定时获取文件的更新时间,并跟上次保存的时间进行比对,不一致就表示发生了变化,poll就用来配置每秒问多少次。
当检测文件不再发生变化,会先缓存起来,等等待一段时间后之后再通知监听者,这个等待时间通过aggregateTimeout配置。
webpack只会监听entry依赖的文件 我们需要尽可能减少需要监听的文件数量和检查频率,当然频率的降低会导致灵敏度下降。
2 自动刷新浏览器
1) use
devServer: { inline: true },
2) note
webpack负责监听文件变化,webpack-dev-server负责刷新浏览器。这些文件会被打包到chunk中,它们会代理客户端向服务器发起WebSocket连接
3 模块热替换
1) action
2) 优点
2) use
devServer: { hot:true//将hot设置为true }, // 需要的插件 plugins: [ new webpack.NamedModulesPlugin(),//显示模块的相对路径 new webpack.HotModuleReplacementPlugin()// 启动热加载功能 ]
if (module.hot) { module.hot.accept('./hot.js', () => { let hot = require('./hot'); document.getElementById('app2').innerHTML = hot + '1'; }) }
3 note
六 区分环境
1 action
在开发网页的时候,一般都会有多套运行环境,例如,在开发过程中方便开发调试的环境。发布到线上给用户使用的运行环境。
线上的环境和开发环境区别主要有以下不同:
2 usage
cross-env
跨平台设置环境变量(后面没有&&)"scripts": { "build-dev": "cross-env NODE_ENV=development webpack --mode development", "build-prod": "cross-env NODE_ENV=production webpack --mode production" }
webpack.base.config.js
合并,生产环境(或者开发环境)的优先级高于webpack.base.config.js
的配置。let merge = require('webpack-merge'); let base = require('./webpack.base.config'); let other = null; if (process.env.NODE_ENV === 'development') { other = require('./webpack.dev.config'); } else { other = require('./webapack.prod.config'); } module.exports = merge(base, other);
webpack.DefinePlugin
定义环境变量基本配置... plugins: [ new webpack.DefinePlugin({ __isDevelopment__: JSON.stringify(process.env.NODE_ENV == 'development') }) ]
output
举例,如果开发和生产环境的参数不同,就会覆盖webpack.base.config.js
里面的配置const path = require('path'); module.exports = { output: { path: path.resolve('./dist'), filename: "[name].dev.[hash:2].js" } };
output
举例)const path = require('path'); module.exports = { output: { path: path.resolve('./dist'), filename: "[name].prod.[hash:8].js" } };
webpack.DefinePlugin
定义的变量(__isDevelopment__
),在入口文件和入口文件引用的其他文件中都可以获取到__isDevelopment__
的值let env = null; if (__isDevelopment__) { env = 'dev'; } else { env = 'prod'; } module.exports = env;
let env = require('./base.js'); if (__isDevelopment__) { console.log('dev'); } else { console.log('prod'); } console.log('env', env); /* prod env prod */
3 note
定义环境变量的值时用 JSON.stringify 包裹字符串的原因是环境变量的值需要是一个由双引号包裹的字符串,而 JSON.stringify('production')的值正好等于'"production"'
七 CDN
CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。
八 Tree Shaking
1 action
tree Shaking 可以用来剔除JavaScript中用不上的死代码。
2 useage
use: { loader: 'babel-loader', query: { presets: [ [ "env", { modules: false //含义是关闭 Babel 的模块转换功能,保留原本的 ES6 模块化语法 } ], "react" ] } },
3 note
需要注意的是它依赖静态的ES6模块化语法,例如通过import和export导入导出。也就是说如果项目代码运行在不支持es6语法的环境上,Tree Shaking也就没有意义了。
九 提取公共代码
1 为什么需要提取公共代码
大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共代码,如果都包含进来会有问题
相同的资源被重复的加载,浪费用户的流量和服务器的成本; 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。 如果能把公共代码抽离成单独文件进行加载能进行优化,可以减少网络传输流量,降低服务器成本
2 如何提取
1) 分类
不同类型的文件,打包后的代码块也不同:
2) usage
optimization: { splitChunks: { cacheGroups: { commons: {// 页面之间的公用代码 chunks: 'initial', minChunks: 2, maxInitialRequests: 5, // The default limit is too small to showcase the effect minSize: 0 // This is example is too small to create commons chunks }, vendor: {// 基础类库 chunks: 'initial', test: /node_modules/, name: "vendor", priority: 10, enforce: true } } } },
require('./utils/utility1.js'); require('./utils/utility2.js'); require('react');
require('./utils/utility2.js'); require('./utils/utility3.js');
require('./utils/utility2.js'); require('./utils/utility3.js');
module.exports = 1;
module.exports = 2;
module.exports = 3;
上述三种代码的生成的结果,如下图:
十 Scope Hoisting
1 action
Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 "作用域提升",是在 Webpack3 中新推出的功能。
2 useage
"build": "webpack --display-optimization-bailout --mode development",
export default 'scope hoist'
import str from './h.js' console.log(str);
3 note
十一 代码分离
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。 有三种常用的代码分离方法:
入口起点和防止重复上面已经提到了,下面我们重点讲一下动态导入
1 action
用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时,一般采用以下原则:
2 usage
import(module)
的语法document.getElementById('play').addEventListener('click',function(){ import('./vedio.js').then(function(video){ let name = video.getName(); console.log(name); }); });
参考文档