坚持周总结系列第一周(2020.4.18)
webpack学习
整体认识
webpack是一个Javascript静态模块打包器,使用webpack打包时,它会递归的构建一个依赖关系图,其中包含程序需要的每个模块,然后将所有这些模块打包成一个或者多个bundle。
入口配置
入口就是整个程序的入口文件所在路径,有两种配置形式。
单文件入口
{
entry:'./src/index.js'
}
// 对象形式
{
entry:{
index:'./src/index.js'
}
}
多文件入口
{
entry:{
index:'./src/index.js',
main:'./src/main.js'
}
}
出口配置
出口就是打包文件最后要保存的文件路径。
{
output:{
filename:'[name][chunkhash:8].js',
path:path.resolve(__dirname+'./dist')
}
}
几个hash的区别
hash:每次打包都会生一个hash,每次打包都会变chunkhash:打包时的每个chunk对应一个chunkhash,可以用于版本管理,chunkhash没变,说明这个chunk中所有依赖的文件都没有发生变化contenthash:每个文件对应一个contenthash,文件内容不变contenthash就不发生变化- 使用场景:
chunkhash一般用于js文件打包后的名字,contenthash一般用于css文件打包后的名字
mode配置
mode选项用于告知webpack使用对应模式的内置优化。
{
mode:'development/production'
}
development: 开发阶段的开启会有利于热更新的处理,识别那个模块变化。会将process.env.NODE_ENV的值设为development,启用NamedChunksPlugin和NamedModulesPlugin。production:生产阶段的开启会有助于帮助模块压缩,处理副作用等一些功能。会将process.env.NODE_ENV的值设为production,启用FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin,OccurrenceOrderPlugin,SideEffectsFlagPlugin和UglifyJsPlugin。
watch文件监听
轮询判断文件的最后编译时间是否变化,如果某个文件发生变化,并不会立刻告诉监听者,而是先缓存起来。
webapck开启监听有两种方式
- 启动
webpack命令式 带上--watch参数,启动监听后,需要⼿动刷新浏览器 - 在配置文件里面设施
watch:true
// 第一种方式
scripts:{
"watch":"webpack --watch"
}
// 第二种方式
{
watch:true,
watchOptions:{//配合watch,只有开启才有作⽤
ignored:/node_modules/,//默认为空,不监听的⽂件或者⽬录,⽀持正则
aggregateTimeout: 300,//监听到⽂件变化后,等300ms再去执⾏,默认300ms
poll: 1000//判断⽂件是否发⽣变化是通过不停的询问系统指定⽂件有没有变化,默认每秒问1次
}
}
sourceMap
源代码与打包后代码的映射关系,在dev模式中,默认开启,关闭的话可以在配置文件中修改。
devtool配置:
eval:速度最快,使⽤eval包裹模块代码source-map:产⽣.map⽂件cheap:较快,不⽤管列的信息,也不包含loader的sourcemapmodule:第三⽅模块,包含loader的sourcemap
// 推荐配置
devtool:"cheap-module-eval-source-map",// 开发环境配置
devtool:"cheap-module-source-map", // 线上⽣成配置(一般不建议开启)
webpackDevServer
开启本地服务,将打包后的模块放在内存中,提升速度。
npm install webpack-dev-server -D
// package.json
"scripts":{
"server":"webpack-dev-server"
}
// webpack.config.js
{
devServer:{
contentBase: "./dist",
open: true,
port: 8081,
proxy:{// 配置跨域
"/api":{
target:"http://localhost:9092"
}
}
}
}
HotModuleReplacement
HMR热模块替换,在不刷新浏览器的情况下,局部更新页面变化部分。
const webpack = require("webpack");
{
devServer:{
contentBase: "./dist",
open: true,
hot:true,
hotOnly:true
},
plugins:[
new webpack.HotModuleReplacementPlugin()
]
}
js模块的热更新,需要特殊处理。
if(module.hot){
module.hot.accept('要热更新处理的模块路径',function(){
// 热更新处理的回调函数
})
}
loader配置
模块解析、模块转换器,用于把模块原内容按照需求转换成新内容。
{
module:{
rules:[
[
test:/\.xxx$/,// 指定匹配规则
use:{
loader:'xxx-loader'// 指定使用的loader
}
]
]
}
}
file-loader
原理是把打包入口中识别出来的资源模块,移动到输出目录,并返回移动后的新地址名称。
{
test:/\.(png|jpe?g|git)$/,
use:{
loader:'file-loader',
options:{
name:'[name][hash:8].[ext]',
outputPath:'images/',
publicPath:''// 这个配置有待继续学习
}
}
}
url-loader
url-loader内部使用了file-loader,所以可以处理所有file-loader能够处理的事情。但是有一个limit配置项,会将文件大小小于limit的图片转换成base64格式字符串,并打包到js里面,有助于减少网络请求。
css-loader、style-loader
css-loader分析css模块之间的关系,最终合并成一个css。
style-loader会把css-loader生成的内容,以style标签方式挂载到页面的head部分。
{
test:/\.css$/,
use:['style-loader','css-loader']// loader执行顺序是从右往左执行
}
postcss-loader、autoprefix
postcss-laoder对css文件进行预处理。
autoprefix给css文件自动增加前缀,适配目标浏览器。
有两种配置方式:
- 所有配置都写在
webpack配置中 - 将
postcss-laoder的配置单独写到postcss.config.js文件中,打包时会自动识别并应用其中的配置内容
// 第一种配置方式
{
test:/\.less$/,
use:['style-loader','css-laoder','less-loader',{
loader:'postcss-loader',
options:{
plugins:()=>[
require('autoprefix')({
overrideBrowserslist:["last 2 versions",">1%"]
})
]
}
}]
}
// 第二种配置方式
{
test:/\.less$/,
use:['style-loader','css-laoder','less-loader','postcss-loader']
}
// posecss.config.js
module.exports={
plugins:[
require('autoprefix')({
overrideBrowserslist: ["last 2 versions", ">1%"]
})
]
}
babel-loader、@babel/core、@babel/preset-env
babel-loader是webpack与babel的通信桥梁,不会做把es6转成es5之类的工作,这部分工作需要@babel/preset-env来做。但是@babel/preset-env转换之后,还有一些新特性,如Promise等没有转换过来,这时候需要借助@babel/polyfill,把es的新特性都引入进来,弥补低版本浏览器中确实的新特性。
npm i babel-loader @babel/core @babel/preset-env -D
npm install --save @babel/polyfill
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
// options选项可以单独配置在.babelrc文件中,打包时会自动识别
}
// .babelrc
{
presets:[
"@babel/preset-env",
{
targets:{
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1"
},
useBuiltIns: "usage"//按需注⼊
}
]
}
// 在入口文件顶部
import "@babel/polyfill";
useBuiltIns选项是babel 7 的新功能,这个选项告诉 babel 如何配置 @babel/polyfill 。 它有三个参数可以使⽤:
entry: 需要在webpack的⼊⼝⽂件⾥import "@babel/polyfill"⼀次。babel会根据你的使⽤情况导⼊垫⽚,没有使⽤的功能不会被导⼊相应的垫⽚。usage: 不需要import,全⾃动检测,但是要安装@babel/polyfill(试验阶段)。false: 如果你import "@babel/polyfill",它不会排除掉没有使⽤的垫⽚,程序体积会庞⼤(不推荐)。
@babel/plugin-transform-runtime
直接引入polyfill的方式,会造成全局变量污染,当开发组件库的使用,使用这种方式就不适合了。
所以推荐闭包⽅式:@babel/plugin-transform-runtime
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
// 修改.babelrc文件
{
"plugins":[
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
plugins配置
plugin可以在webpack运行到某个阶段的时候,做一些处理,类似于生命周期的概念,作用于整个构建过程。
扩展插件,能够给在webpack构建流程中的特性时机注入扩展逻辑来改变构建结构或者执行你想要做的事情。
HtmlWebpackPlugin
HtmlWebpackPlugin会在打包结束后,自动生成一个html文件,并把打包成成的js模块引入到该html文件中。
npm install --save-dev html-webpack-plugin
const htmlWebpackPlugin = require("html-webpack-plugin");
{
plugins:[
new htmlWebpackPlugin({
title:'My App',
filename:'app.html',
template:'./src/index.html'
})
]
}
// 在index.html中使用title配置
<title><%= htmlWebpackPlugin.options.title %></title>
HtmlWebpackPlugin配置项:
title: ⽤来⽣成⻚⾯的title元素filename: 输出的HTML⽂件名template: 模板⽂件路径inject:注入的资源放在HTML文件中的位置favicon: 添加特定的favicon路径到输出的 HTML ⽂件中showErrors: 错误信息是否写⼊到HTML⻚⾯中chunks: 允许只添加某些块chunksSortMode: 允许控制块在添加到⻚⾯之前的排序⽅式excludeChunks: 允许跳过某些块
clean-webpack-plugin
每次打包前,先清理上次打包内容。
npm install --save-dev clean-webpack-plugin
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
...
{
plugins: [
new CleanWebpackPlugin()
]
}
mini-css-extract-plugin
将css文件抽离成单独的文件。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
{
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
...
plugins:[
new MiniCssExtractPlugin({
filename:"[name][contenthash:8].css"
})
]
}
treeShaking
只支持ES module的引入方式,在开发模式下,不会将没有用到的代码去掉。
{
optimization:{
usedExports:true// 开启treeShaking
}
}
// 使用treeShaking,像import './index.css'这类方式引入的静态资源也会被优化掉
// 解决办法,在package.json中加入限制配置
{
"sideEffects":false// 正常情况对所有模块进行treeShaking
"sideEffects":['*.css','@babel/polifill']// 不对配置中的模块进行treeShaking
}
代码分割codeSplitting
{
optimization:{
splitChunks:{
chunks:'all'// 对所有模块有效
miniSize:30000,// 当模块大于30kb时启动分隔
minChunks:1,// 生成的chunk文件至少有一个chunk引用
automaticNameDelimiter: '~',//打包分割符号
name: true,//打包后的名称
cacheGroups: {//缓存组
vendors: {
test: /[\\/]node_modules[\\/]/,
name:"vendor", // 要缓存的 分隔出来的 chunk 名称
priority: -10//缓存组优先级 数字越⼤,优先级越⾼
}
}
}
}
}
// 启动代码分割之后,要在HtmlWebpackPlugin中的chunks配置项加上单独打包的chunk名称
针对环境配置webpack
- 在
scripts命令中使用--config指定要使用的webpack配置文件,达到不同命令使用不同配置 - 基于环境变量
- 在
scripts命令中传入环境变量--env.production - 采用环境变量生成不同配置
- 在
npm install webpack-merge -D
const merge = require("webpack-merge")
module.exports=(env)=>{
if(env && env.production){
return merge(commonConfig,prodCOnfig)
}else{
return merge(commonConfig,devConfig)
}
}
webpack原理浅析
// 找到入口文件,分析内容,有依赖的话,拿到依赖路径,转换代码(在浏览器中可以运行的代码)
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const entry = entryFile => {
const content = fs.readFileSync(entryFile, 'utf-8')
// 将文件内容转换成AST
const ast = parser.parse(content, {
sourceType: "module"
})
const dependecies = {}
// 分析出依赖项路径
traverse(ast, {
// 以函数的方式
ImportDeclaration({ node }) {
const dirname = path.dirname(entryFile)
const newPath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = newPath
}
})
// 将AST转换成浏览器可执行代码
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
entryFile,
dependecies,
code
}
}
// 分析出所有依赖关系
const deps = entryFile => {
const info = entry(entryFile)
const modules = []
modules.push(info)
// 递归遍历,找出所有依赖模块
for (let i = 0; i < modules.length; i++) {
const item = modules[i]
const { dependecies } = item
if (dependecies) {
for (let j in dependecies) {
modules.push(entry(dependecies[j]))
}
}
}
// 将依赖模块数组转换成对象形式
const obj = {}
modules.forEach(item => {
obj[item.entryFile] = {
dependecies: item.dependecies,
code: item.code
}
})
return obj
}
// 生成代码
const genCode = entryFile => {
const obj = deps(entryFile)
// 将对象转成字符串,便于写入文件
const graph = JSON.stringify(obj)
const bundle = `(function(graph){
// 构造require函数
function require(module) {
function localRequire(relativePath) {
// 处理依赖路径
return require(graph[module].dependecies[relativePath])
}
// 构造exports对象
var exports={};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('${entryFile}')
})(${graph})`;
// 将打包结果写入到出口文件
fs.writeFileSync(path.resolve(__dirname, './dist/main.js'), bundle, 'utf-8')
}
genCode('./src/index.js')
编写一个Loader
Loader就是一个声明式函数,不能使用箭头函数,因为箭头函数会改变this指向。
简单Loader实现
// index.js
console.log('hello kaikeba!')
// my-loader.js
module.exports=function(source){
console.log(source)// 匹配到的文件内容
return source.replace('kaikeba','webpack')// 对内容处理之后返回
}
// 配置文件中使用Loader
const path=require('path')
module:{
tules:[
{
test:/\.js$/,
use:path.resolve(__dirname,'./loader/my-loader.js')
}
]
}
给Loader配置参数
Loader接收参数有两种方式:this.query、loader-utils
// 配置参数
{
test:/\.js$/,
use:path.resolve(__dirname,'./loader/my-loader.js'),
name:'开课吧'
}
// my-loader.js
const loaderUtils=require('loader-utils')// 官方推荐工具
module.exports=function(source){
let name1=this.query.name// 通过this.query获取配置参数
const options=loaderUtils.getOptions(this)
let name2=options.name// 通过loader-utils获取配置参数
return source.replace('kaikeba','webpack')
}
this.callback使用
- 使用
this.callback返回多个信息
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
- 配合
this.async处理loader中的异步逻辑
const callback = this.async();
setTimeout(() => {
const result = source.replace("kkb", options.name);
callback(null, result);
}, 3000);
Laoder的执行顺序
按照⾃下⽽上,⾃右到左顺序执行。
处理Loader的引入路径问题
resolveLoader:{
modules:['node_modules','./loader']// 会去配置的路径中找自定义loader文件
},
module:{
rules:[
{
test:/\.js$/,
use:['my-loader']
}
]
}
编写一个Plugin
Plugin在打包的某个时刻,帮助我们处理一些需求的机制。plugin是一个类,里面包含一个apply函数,接收compiler参数。
// 使用plugin
const MyPlugin=require('./plugin/my-plugin.js')
plugins:[new MyPlugin({
name:'开课吧'
})]
// 编写plugin
class MyPlugin{
constructor(options){
console.log(options)// 获取配置参数
this.options=optioins
}
apply(compiler){
// 异步钩子使用
compiler.hooks.emit.tapAsync('MyPlugin',(compilation,cb)=>{
compilation.assets['plugin.txt']={// 本次编译生成的资源
source:()=>{
return '我是自定义plugin生成的文件。'// 添加的文件内容
},
size:()=>{
return 1024// 文件的大小
}
}
cb()// 执行回调,进入下一个钩子
})
// 同步钩子调用
compiler.hooks.compile.tap('MyPlugin',compilation=>{
console.log('同步钩子调用了')
})
}
}
module.exports=MyPlugin

被折叠的 条评论
为什么被折叠?



