一、目标
完成webpack搭建,通过解析引擎,将业务文件打包成koa可以解析出来的产物文件,解析后输出页面;在打包过程中,根据环境区分具体配置
二、基础配置项
1. entry
告知webpack从哪个文件开始构建
-
单页面:
只有一个入口文件,输出一个chunk,路由由前端控制,具体配置:
{ entry: './src/main.js' } -
多页面:
多页面就是具备多个入口,输出多个chunk,每个页面独立运行、单独部署,由后端模板渲染,具体配置:
{ entry: { index: './src/pages/index.js', admin: './src/pages/admin.js', } }多页面需要多个html文件,通常使用HtmlWebpackPlugin来进行对应,chunks决定了注入哪个入口的bundle,需要和entry中设定的入口相对应
new HtmlWebpackPlugin({ filename: 'index.tpl', template: path.resolve(process.cwd(), "./app/view/entry.tpl"), chunks: ["index"] }),
2. module
模块解析,决定了要对哪些模块要使用哪些loader进行解析。
如果配置了多个loader,他的执行顺序是从右往左执行、从下往上执行;因为webpack认为loader是函数组合,例:use: ["style-loader", "css-loader"]的执行顺序类似于style(css(source))
module: {
rules: [
{
test: /\.vue$/,
use: {
// vue-loader必须配合 vueLoaderPlugin使用,因为vue-loader需要插件配合处理SFC(单文件组件)的拆分逻辑
loader: "vue-loader"
}
}
]
},
loader是如何进行解析的,又是从哪个文件获取到的?
webpack解析loader的逻辑和node解析模块的逻辑一样(loader的解析逻辑 ≈ node.js的require解析逻辑)
默认从 node_modules/目录逐级向上查找。如果在loader中填写绝对路径,会直接用指定的路径,不会去node_modules/中查找
3. output
output的基础配置:
output: {
filename: "/js/main.js",
path: "",
publicPath: "",
},
在filename中可以使用chunkhash,来实现代码改了就自动更新、没改就不重新下载,因为浏览器的缓存策略是文件名变了就重新下载、文件名没变就走缓存;而chunkhash是只要内容变了hash就变
- 开发环境
在开发环境的配置中,由于使用的'webpack-dev-middleware',资源是存在内存里面,而不是从磁盘读取的,所以使用的是完整Http地址;
且为了解决 "window is not defined",要添加globalObject: "this"配置;
不同环境的全局对象不一样,webpack默认用window作为全局对象,node.js使用global作为全局对象,添加配置改成this可以兼容多环境,使在不同环境下,this可以准确访问到全局对象
- 生产环境
由于生产环境通常由Nginx、CDN等统一托管,所以在output的publicPath中使用的是相对路径;
且生产环境里面添加crossOriginLoading: "anonymous"配置解决跨域问题
crossOriginLoading: "anonymous"在解决什么跨域问题?如果不配置会发生什么?为什么只在生产环境中配置?
这里说的跨域不是ajax跨域而是浏览器使用< script >标签加载跨域JS资源;我们生产环境可能会出现主站和CDN使用不同域名、script跨域加载,例:
| 资源类型 | 来源 |
|---|---|
| index.html | www.xxxxx.com |
| JS静态资源 | cdn.xxxxx.com |
在开发环境中,使用完整http地址访问内存,是同源的不需要;而生产环境通常会静态资源分域名、上CDN、多子域名部署,所以要在生产环境配置跨域
4. resolve
用于配置模块解析的具体行为,比如webpack如何把import里的字符串变为文件路径的,即把'./app'解析成'/src/app'的
常用的是:
extensions:会自动补全文件后缀,按数组里面的顺序查找,找到第一个存在的文件就停止;数组定义的顺序很重要、越常用的放前面
alias:给路径起别名,在解析的时候、会先查找是否有匹配的alias,有的话替换为真实路径,再进行解析
5. plugins
对webpack生命周期的扩展,常用的插件:HtmlWebpackPlugin(自动生产HTML并注入打包后的js)、CleanWebpackPlugin(清理之前的构建结果)、MiniCssExtractPlugin(提取css公共部分)
6. optimization
控制webpack如何优化输出结果,包含代码分割、模块合并、缓存、treeShaking、压缩等优化策略
-
splitChunks:
可以把公共文件抽离成单独文件,即把多个chunk之间"共享的模块"抽成新的chunk
核心目的不是为了生成更多的包,而是为了让缓存命中率最大化,而是减少打包重复率、提高缓存利用率
splitChunks会根据四个维度来决定拆谁:
- priority优先级高的先处理
- minChunks复用次数多的优先拆
- minSize满足最小模块体积的,但是小于模块体积的不考虑拆分、拆了反而会增加请求成本
- 剩余并行请求数限制,如果拆分太多导致请求超限,会放弃拆分
所以我们可以根据这四个维度来配置cacheGroups自定义拆包规则:
-
framework(1和2可以拆成一个包)
管理库等可以从三方组件库中单独拆分出来,这些稳定且复用率高,通常只包含核心运行时和框架内部依赖,不会包含业务代码或通用工具库;
配置信息:
test: /[\\/]node_modules[\\/](vue|vue-router|vuex)/, -
rendor
node_modules 第三方依赖库,必须拆分出来,因为更新频率低、体积大、缓存价值高
配置信息:
test: /[\\/]node_modules[\\/]/, -
common
公共组件,多个页面共同使用的某些utils或者组件
-
业务组件代码
-
runtimeChunk:
用于把webpack运行时代码单独抽离、避免业务代码变动导致缓存失效
三、环境配置
-
开发环境
开发环境配置的核心目标是启动快、热更新快、调试体验好、不污染生产产物
配置中使用热更新(在有修改的时候,不刷新整个页面,只替换修改的模块)。热更新的配置分为以下几个部分:
-
devServer配置
给HMR客户端用的连接信息,因为热更新底层靠的是浏览器和webpack-dev-server之间的websocket长连接
把打包结果放在内存中,好处是不走磁盘IO、编译更快、热更新更快
下列代码是我们自己定义的一个常量,使用的是webpack-hot-middleware,用于给HMR客户端拼接连接地址使用:
const DEV_SERVER_CONFIG = { HOST: "127.0.0.1", PORT: 9002, HMR_PATH: "__webpack_hmr", TIMEOUT: 20000 };对于HMR_PATH这个路径的设定不是必须是这个,但是hotMiddleware会默认监听'
__webpack_hmr',浏览器也会自动连接到这个路径;可以做修改修改的同时要注意下面注入到HMR客户端的路径,必须要匹配上,否则会链接不上。还有一种方法是使用webpack-dev-server设定,webpack自动开启websocket、注入HMR
devserver: { hot:true } -
HMR(热更新)
正常情况下webpack打包之后,浏览器只执行entry中的入口文件;下面这段代码就是手动往每个入口文件中注入HMR客户端代码(核心代码)
Object.keys(baseConfig.entry).forEach(v => { if (v !== "vendor") { baseConfig.entry[v] = [ baseConfig.entry[v], `webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}` ]; } });之所以在注入的时候排除掉rendor,是因为在分包的时候rendor中都是第三方库,这些第三方库基本不会变;如果给rendor添加HMR客户端会增加bundle体积、可能会影响缓存、没有意义
在向入口文件注入HMR客户端代码的同时,要添加HotModuleReplacementPlugin,告知webpack开启模块级别的替换能力,如果没加这个plugin,即使客户端连上了websocket也只会整页刷新不会进行模块替换
为了实现热更新,浏览器要经历哪几个步骤?
- 和dev-server建立websocket
- 监听代码改动
- 下载更新模块
- 替换旧模块
-
devtool
控制source-map生成方式、生成时机、映射精度和输出位置
devtool值的核心组合规则是
[inline-][eval-][hidden-][nosources-][cheap-][module-]source-map;除了eval和false以外必须以source-map结尾,以下是几种常见的devtool的比对:devtool 构建速度 调试精度 适用环境 评价 false 最快 没有 正式生产环境 推荐用于性能最大化的生产版本 eval 快 一般 开发 推荐用于开发版本以最高性能进行 eval-cheap-module-source-map 一般 精确到行 开发 开发构建的权衡选择 cheap-module-source-map 慢 精确到行 开发 source-map 最慢 行+列 生产环境排错 hidden-source-map 最慢 行+列 生产环境线上监控
-
-
生产环境
生产环境追求的是性能、缓存和安全;本次构建中使用了thread-loader/happypack来做多线程编译,使用TerserPlugin来做并发压缩和缓存。
-
thread-loader
作用:把后续loader放到worker线程池里面执行,适合比较耗时的loader,比如:babel-loader;
需要注意的是thread-loader必须放在babel-loader前面,如果并发执行的loader项目很小(例:css-loader)反而会因为进程创建有开销导致变慢
use: [ { loader: "thread-loader", options: { workers: os.cpus().length, // 每个线程并发处理的工作数,默认为20 workerParallelJobs: 30, // 闲置时定时删除 worker 进程、默认为500(毫秒) // 以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在 poolTimeout: 2000, // 工作池分配给worker的工作数量。默认为200. // 降低这个数值会降低总体的效率,但是会提升工作分布更均一 poolParallelJobs: 50 } }, { loader: "babel-loader", options: { presets: ["@babel/preset-env"], plugins: [["@babel/plugin-transform-runtime"]] } } ] -
happypack
happypack在使用时,需要在plugin和loader中同时添加
use: ["happypack/loader?id=js"] ... ... plugin: [ // 多线程打包js,加快打包速度 new HappyPack({ debug: false, threadPool: HappyPack.ThreadPool({ size: os.cpus().length }), id: "js", loaders: [ `babel-loader?${JSON.stringify({ presets: ["@babel/preset-env"], plugins: ["@babel/plugin-transform-runtime"] })}` ] }), ] -
TerserPlugin
作用是压缩JS、删除注释和console;可以多进程并发压缩、缩短压缩阶段耗时;TerserPlugin支持缓存,对没变化的文件会跳过压缩
new TerserPlugin({ // 启用缓存来加速构建过程 cache: true, // 利用多核 CPU 的优势来加快压缩速度 parallel: true, terserOptions: { compress: { // 去掉 console.log 内容 drop_console: true }, format: { // 移除注释 comments: true } }, // 是否将注释提取到单独文件中 extractComments: false })
-
四、webpack启动
-
开发环境
本次我们开发环境的启动构建不是直接使用webpack-dev-server,而是基于webpack-dev-server的底层实现完成一个属于我们自己的 'webpack-dev-server';以下是具体步骤:
-
创建Express实例
const app = express();本质上是创建一个HTTP服务器,负责监听端口、处理请求、返回静态资源和建立HMR通讯
-
创建compiler
const compiler = webpack(webpackConfig);获取之前在开发环境webpack中导出的
{ webpackConfig, DEV_SERVER_CONFIG },并创建一个compiler实例(此时仍处于准备阶段,还没开始构建) -
指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")))用于访问磁盘文件;但是我们使用的'webpack-dev-middleware',默认使用内存文件系统,所以这个目录大多数时候是空的,不会写到磁盘中
注:HMR只是浏览器和服务器之间的通讯机制,是否写入磁盘和HMR没有关系
-
引入 devMiddleware 中间件(核心)
writeToDisk: filePath => filePath.endsWith(".tpl"),devMiddleware中的这个配置会将tpl文件写入到真实文件中为什么不全内存,而是要将tpl写入到真实文件中?
因为某些后端模板需要真实文件。。。。用来干嘛?有哪些模板需要?为什么需要
headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": "X-Request-With, content-type, Authorization" },为什么要加这些header?
- 这部分header属于响应头,作用是解决浏览器跨域问题;因为浏览器的同源策略机制,使得协议+域名+端口 必须完全一致,否则请求会被浏览器拦截
- 前端和后端不处于同一个端口下,属于跨域,加上这些响应头开发时方便调试
- 配置了这个header所有由
webpack-dev-middleware返回的资源都会自动带上这些
这些header都是用来干嘛的?
-
Access-Control-Allow-Origin:
· 告知浏览器允许哪些源访问我的资源,设置为
*就是允许任何域名访问这个资源,设置特定域名就是只允许这个域名访问· 是CORS(跨域资源共享)响应头,加在返回JS资源的服务器响应头里面,只在跨域请求时才检查
· 如果有跨域获取资源,但是不需要读取内容,即使没有配置Access-Control-Allow-Origin也能加载执行,只是在报错的时候拿不到真实的报错信息
· 如果有跨域、设置了crossorigin、但没有配置Access-Control-Allow-Origin,浏览器会直接拦截加载、script加载会失败、控制台报CORS错误
-
Access-Control-Allow-Methods:
· 允许跨域请求使用哪些HTTP方法
-
Access-Control-Allow-Headers:
· 允许客户端发送哪些自定义请求头
-
引入 hotMiddleware 中间件
负责浏览器和服务器之间的通讯,采用SSE(Server Sent Events)机制
hotMiddleware(compiler, { path: `/${DEV_SERVER_CONFIG.HMR_PATH}`, log: () => {} })path是我们在webpack中定义的HMR通讯路径,浏览器会请求这个路径
-
启动 devserver
开始监听端口
const port = DEV_SERVER_CONFIG.PORT; app.listen(port, () => { console.log(`app listening on port ${port}`); });为什么在启动devserver的时候只写了监听端口的代码?
- 在devMiddleware内部已经调用compiler.watch(),也就意味着webpack已经开始监听文件变化了
- express只提供Http能力,职责只有监听端口、接收请求、返回资源
所以监听端口只是为了打开网络入口,真正的构建启动时间点实在devMiddleware
-
-
生产环境
相比于开发环境在启动构建的时候配置的监听文件改动、热更新之类的,生产环境比较简洁明了的多;只是启动一次构建、然后执行callback
webpack(webpackProdConfig, (err, stats) => { if (err) { console.log(err); return; } process.stdout.write( `${stats.toString({ // 在控制台输出色彩信息 colors: true, // 不显示每个模块的打包信息 modules: false, // 不显示子编译任务的信息 children: false, // 不显示每个代码块的信息 chunks: false, // 显示代码块中模块的信息 chunkModules: true })}\n` ); });这个callback在构建完成之后进行回调,在callback捕获的err中只包含致命错误,例如语法错误、插件崩溃等;stats中记录的则是模板编译错误,stats包含了整个构建结果的统计信息,例如模块数量、chunk信息等;
使用stats.toString()可以把stats对象格式化为可读的文本,也就是我们在控制台看到的打包结果。
五、开发环境和生产环境的差异
-
构建位置
开发环境是在内存中,生产环境是写入磁盘
-
是否监听
开发环境监听改变,生产环境执行完就结束
-
是否压缩
开发环境通常不需要进行压缩,生产环境进行压缩
-
启用HMR
开发环境为了更好的调试,启用HMR;生产环境不需要