傻瓜式webpack教程(一步步从零开始用)
傻瓜式webpack教程(一步步从零开始用)脚手架github.com/luoxue-vict…我把每一课都切成了不同的分支,大家可以根据课时一步步学习
开篇
很多人都或多或少使用过 webpack,但是很少有人能够系统的学习 webpack 配置,遇到错误的时候就会一脸懵,不知道从哪查起?性能优化时也不知道能做什么,网上的优化教程是不是符合自己的项目?等一系列问题!本文从最基础配置一步步到一个完善的大型项目的过程。让你对 webpack 再也不会畏惧,让它真正成为你的得力助手!
本文从下面几个课题来实现
- 课题 1:初探 webpack?探究 webpack 打包原理
- 课题 2:搭建开发环境跟生产环境
- 课题 3:基础配置之loader
- 课时 4:webpack性能优化
- 课时 5:手写loader实现可选链
- 课时 6:webpack编译优化
- 课时 7:多页面配置
- 课时 8:手写一个webpack插件
- 课时 9:构建 ssr
项目地址
github.com/luoxue-vict…
我把每一课都切成了不同的分支,大家可以根据课时一步步学习
脚手架
npm i -g webpack-box
使用
webpack-box dev # 开发环境 webpack-box build # 生产环境 webpack-box dll # 编译差分包 webpack-box dev index # 指定页面编译(多页面) webpack-box build index # 指定页面编译(多页面) webpack-box build index --report # 开启打包分析 webpack-box build:ssr # 编译ssr webpack-box ssr:server # 在 server 端运行
在 package.json 中使用
{ "scripts": { "dev": "webpack-box dev" "build": "webpack-box build" "dll": "webpack-box dll" "build:ssr": "webpack-box build:ssr" "ssr:server": "webpack-box ssr:server" } }
使用
npm run build --report # 开启打包分析
扩展配置
box.config.js
module.exports = function (config) { /** * @param {object} dll 开启差分包 * @param {object} pages 多页面配置 通过 box run/build index 来使用 * @param {function} chainWebpack * @param {string} entry 入口 * @param {string} output 出口 * @param {string} publicPath * @param {string} port */ return { entry: 'src/main.js' output: 'dist' publicPath: '/common/' port: 8888 dll: { venders: ['vue' 'react'] } pages: { index: { entry: 'src/main.js' template: 'public/index.html' filename: 'index.html' } index2: { entry: 'src/main.js' template: 'public/index2.html' filename: 'index2.html' } } chainWebpack(config) { } } }
课题 1:初探 webpack?探究 webpack 打包原理
想要学好 webpack,我们首先要了解 webpack 的机制,我们先从js加载css开始学习。
我们从下面这个小练习开始走进 webpack 吧
在 index.js 中引入 index.css
const css = require('./index.css') console.log(css)
css 文件并不能被 js 识别,webpack 也不例外,上述的写法不出意外会报错
我们如何让 webpack 识别 css 呢,答案就在 webpack 给我们提供了 loader 机制,可以让我们通过loader 将任意的文件转成 webpack 可以识别的文件
本章主要讲解
- webpack 基础配置
- 解析 bundle 如何加载模块
- 动态 import 加载原理
- 使用 webpack-chain 重写配置
- 课时 1 小结
webpack 基础配置
需要的依赖包
package.json
{ "scripts": { "dev": "cross-env NODE_ENV=development webpack" // 开发环境 "build": "cross-env NODE_ENV=production webpack" // 生产环境 } "dependencies": { "cross-env": "^6.0.3" // 兼容各种环境 "css-loader": "^3.2.0" "rimraf": "^3.0.0" // 删除文件 "webpack": "^4.41.2" } "devDependencies": { "webpack-cli": "^3.3.10" } }
webpack 基础配置
webpack.config.js
const path = require('path'); const rimraf = require('rimraf'); // 删除 dist 目录 rimraf.sync('dist'); // webpack 配置 module.exports = { entry: './src/index' mode: process.env.NODE_ENV output: { filename: 'bundle.js' path: path.resolve(__dirname 'dist') } };
css 引入到 js
src/index.js
const css = require('css-loader!./index.css'); const a = 100; console.log(a css);
测试 css
src/index.css
body { width: 100%; height: 100vh; background-color: orange; }
解析 bundle 如何加载模块
我删掉了一些注释跟一些干扰内容,这样看起来会更清晰一点
- bundle 是一个立即执行函数,可以认为它是把所有模块捆绑在一起的一个巨型模块。
- webpack 将所有模块打包成了 bundle 的依赖,通过一个对象注入
- 0 模块 就是入口
- webpack 通过 __webpack_require__ 引入模块
- __webpack_require__ 就是我们使用的 require,被 webpack 封装了一层
dist/bundle.js
(function(modules) { function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = (installedModules[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__((__webpack_require__.s = 0)); })({ './src/index.js': function(module exports __webpack_require__) { eval(` const css = __webpack_require__("./src/style/index.css") const a = 100; console.log(a css) `); } './src/style/index.css': function(module exports __webpack_require__) { eval(` exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false); exports.push([module.i "body { width: 100%; height: 100vh; background-color: orange; }" ""]); `); } 0: function(module exports __webpack_require__) { module.exports = __webpack_require__('./src/index.js'); } });
动态 import 加载原理
如果我们把 index.js 的 require 改成 import 会发生什么?
我们知道 import 跟 require 的区别是,import 是动态加载只有在用到的时候才会去加载,而 require 只要声明了就会加载,webpack 遇到了 require 就会把它当成一个模块加载到 bundle的依赖里
那么问题来了,如果我们使用了 import 去引用一个模块,它是如何加载的呢?
require 改成 import()
src/index.js
// const css = require('css-loader!./index.css'); const css = import('css-loader!./index.css'); const a = 100; console.log(a css);
动态加载打包结果
除了正常的 bundle 之外,我们还可以看见一个 0.boundle.js
0.boundle.js 就是我们的动态加载的 index.css 模块
|-- bundle.js |-- 0.boundle.js
动态模块
0.boundle.js
这个文件就是把我们 import 的模块放进了一个单独的 js 文件中
(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ [0] { './node_modules/css-loader/dist/runtime/api.js': function( module exports __webpack_require__ ) { 'use strict'; eval(` ... `); } './src/style/index.css': function(module exports __webpack_require__) { eval(` exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false)); exports.push([module.i \`body { width: 100%; height: 100vh; background-color: orange; } "\`] `); } } ]);
动态模块加载逻辑
我们再看下 dist/bundle.js
方便理解,我把大部分代码和注释都删掉了
原理很简单,就是利用的 jsonp 的实现原理加载模块,只是在这里并不是从 server 拿数据而是从其他模块中
- 调用模块时会在 window 上注册一个 webpackJsonp 数组,window['webpackJsonp'] = window['webpackJsonp'] || []
- 当我们 import时,webpack 会调用 __webpack_require__.e(0) 方法,也就是 requireEnsure
- webpack 会动态创建一个 script 标签去加载这个模块,加载成功后会将该模块注入到 webpackJsonp 中
- webpackJsonp.push 会调用 webpackJsonpCallback 拿到模块
- 模块加载完(then)再使用 __webpack_require__ 获取模块
(function(modules) { function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var moduleId chunkId i = 0 resolves = []; for (; i < chunkIds.length; i ) { chunkId = chunkIds[i]; if ( Object.prototype.hasOwnProperty.call(installedChunks chunkId) && installedChunks[chunkId] ) { resolves.push(installedChunks[chunkId][0]); } // 模块安装完 installedChunks[chunkId] = 0; } for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) { // 执行所有 promise 的 resolve 函数 resolves.shift()(); } } function jsonpScriptSrc(chunkId) { return __webpack_require__.p '' ({}[chunkId] || chunkId) '.bundle.js'; } function __webpack_require__(moduleId) { // ... } __webpack_require__.e = function requireEnsure(chunkId) { var promises = []; // ... var script = document.createElement('script'); var onScriptComplete; script.charset = 'utf-8'; script.timeout = 120; script.src = jsonpScriptSrc(chunkId); onScriptComplete = function(event) { // 处理异常,消除副作用 // ... }; var timeout = setTimeout(function() { onScriptComplete({ type: 'timeout' target: script }); } 120000); script.onerror = script.onload = onScriptComplete; document.head.appendChild(script); // ... // 动态加载模块 return Promise.all(promises); }; var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []); // 重写数组 push 方法 jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for (var i = 0; i < jsonpArray.length; i ) webpackJsonpCallback(jsonpArray[i]); return __webpack_require__((__webpack_require__.s = 0)); })({ './src/index.js': function(module exports __webpack_require__) { eval(` const css = __webpack_require__.e(0).then(__webpack_require__.t.bind(null "./src/style/index.css" 7)) const a = 100; console.log(a css) `); } 0: function(module exports __webpack_require__) { eval(`module.exports = __webpack_require__("./src/index.js");`); } });
使用 webpack-chain 重写配置
我们用 webpack-chain 来写 webpack 的配置,原因是 webpack-chain 的方式更加灵活
官方解释
webpack-chain 尝试通过提供可链式或顺流式的 API 创建和修改 webpack 配置。API 的 Key 部分可以由用户指定的名称引用,这有助于跨项目修改配置方式的标准化。
const path = require('path'); const rimraf = require('rimraf'); const Config = require('webpack-chain'); const config = new Config(); const resolve = src => { return path.join(process.cwd() src); }; // 删除 dist 目录 rimraf.sync('dist'); config // 入口 .entry('src/index') .add(resolve('src/index.js')) .end() // 模式 // .mode(process.env.NODE_ENV) 等价下面 .set('mode' process.env.NODE_ENV) // 出口 .output.path(resolve('dist')) .filename('[name].bundle.js'); config.module .rule('css') .test(/\.css$/) .use('css') .loader('css-loader'); module.exports = config.toConfig();
课时 1 小结
至此课时 1 已经结束了,我们主要做了以下事情
- webpack 基础配置
- 将 css 通过 css-loader 打包进 js 中
- 解析 bundle 如何加载模块的
- webpack 如何实现的动态加载模块
学习一个工具我们不仅要看懂它的配置,还要对它的原理一起了解,只有学到框架的精髓,我们才能应对如今大前端如此迅猛的发展。
课题 2:搭建开发环境跟生产环境
本章提要:
- 目录
- 实现可插拔配置
- 构建生产环境
- 构建开发环境(devServer)
- 提取 css
- 自动生成 html
- 项目测试
目录
│── build │ │── base.js // 公共部分 │ │── build.js │ └── dev.js │── config │ │── base.js // 基础配置 │ │── css.js // css 配置 │ │── HtmlWebpackPlugin.js // html 配置 │ └── MiniCssExtractPlugin.js // 提取css │── public // 公共资源 │ └── index.html // html 模版 └── src // 开发目录 │── style │ └── index.css └── main.js // 主入口
实现可插拔配置
package.json
{ "scripts": { "dev": "cross-env NODE_ENV=development node build/dev.js" "build": "cross-env NODE_ENV=production node build/build.js" } "dependencies": { "cross-env": "^6.0.3" "css-loader": "^3.2.0" "cssnano": "^4.1.10" "ora": "^4.0.3" "rimraf": "^3.0.0" "webpack": "^4.41.2" } "devDependencies": { "extract-text-webpack-plugin": "^3.0.2" "html-webpack-plugin": "^3.2.0" "mini-css-extract-plugin": "^0.8.0" "vue-cli-plugin-commitlint": "^1.0.4" "webpack-chain": "^6.0.0" "webpack-cli": "^3.3.10" "webpack-dev-server": "^3.9.0" } }
build/base.js
const { findSync } = require('../lib'); const Config = require('webpack-chain'); const config = new Config(); const files = findSync('config'); const path = require('path'); const resolve = p => { return path.join(process.cwd() p); }; module.exports = () => { const map = new Map(); files.map(_ => { const name = _.split('/') .pop() .replace('.js' ''); return map.set(name require(_)(config resolve)); }); map.forEach(v => v()); return config; };
构建生产环境
build/build.js
const rimraf = require('rimraf'); const ora = require('ora'); const chalk = require('chalk'); const path = require('path'); // 删除 dist 目录 rimraf.sync(path.join(process.cwd() 'dist')); const config = require('./base')(); const webpack = require('webpack'); const spinner = ora('开始构建项目...'); spinner.start(); webpack(config.toConfig() function(err stats) { spinner.stop(); if (err) throw err; process.stdout.write( stats.toString({ colors: true modules: false children: false chunks: false chunkModules: false }) '\n\n' ); if (stats.hasErrors()) { console.log(chalk.red('构建失败\n')); process.exit(1); } console.log(chalk.cyan('build完成\n')); });
构建开发环境(devServer)
build/dev.js
const config = require('./base')(); const webpack = require('webpack'); const chalk = require('chalk'); const WebpackDevServer = require('webpack-dev-server'); const port = 8080; const publicPath = '/common/'; config.devServer .quiet(true) .hot(true) .https(false) .disableHostCheck(true) .publicPath(publicPath) .clientLogLevel('none'); const compiler = webpack(config.toConfig()); // 拿到 devServer 参数 const chainDevServer = compiler.options.devServer; const server = new WebpackDevServer( compiler Object.assign(chainDevServer {}) ); ['SIGINT' 'SIGTERM'].forEach(signal => { process.on(signal () => { server.close(() => { process.exit(0); }); }); }); // 监听端口 server.listen(port); new Promise(() => { compiler.hooks.done.tap('dev' stats => { const empty = ' '; const common = `App running at: - Local: http://127.0.0.1:${port}${publicPath}\n`; console.log(chalk.cyan('\n' empty common)); }); });
提取 css
config/css.js
css 提取 loader 配置
module.exports = (config resolve) => { return (lang test) => { const baseRule = config.module.rule(lang).test(test); const normalRule = baseRule.oneOf('normal'); applyLoaders(normalRule); function applyLoaders(rule) { rule .use('extract-css-loader') .loader(require('mini-css-extract-plugin').loader) .options({ publicPath: './' }); rule .use('css-loader') .loader('css-loader') .options({}); } }; };
css 提取插件 MiniCssExtractPlugin
config/MiniCssExtractPlugin.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = (config resolve) => { return () => { config .oneOf('normal') .plugin('mini-css-extract') .use(MiniCssExtractPlugin); }; };
自动生成 html
config/HtmlWebpackPlugin.js
const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = (config resolve) => { return () => { config.plugin('html').use(HtmlWebpackPlugin [ { template: 'public/index.html' } ]); }; };
项目测试
测试 html 模板
public/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>learn_webpack</title> <body></body> </html>
测试 css 模板
src/style/index.css
.test { width: 200px; height: 200px; color: red; background-color: orange; }
程序入口
src/main.js
require('./style/index.css'); const h2 = document.createElement('h2'); h2.className = 'test'; h2.innerText = 'test'; document.body.append(h2);
课题 3:基础配置之loader
本章提要:
- 配置 babel
- 使用 babel 配置 ts
- ts 静态类型检查
- 友好错误提示插件
- 配置样式,style,css、less、sass、postcss 等
- postcss 配置
- 编译前后 css 对比
- 配置 autoprefixer
- 开启 source map
目录
增加以下文件
│──── config // 配置目录 │ │── babelLoader.js // babel-loader 配置 │ │── ForkTsChecker.js // ts 静态检查 │ │── FriendlyErrorsWebpackPlugin.js // 友好错误提示 │ └── style │──── src // 开发目录 │ │── style │ │ │── app.css │ │ │── index.less // 测试 less │ │ │── index.scss // 测试 sass │ │ └── index.postcss // 测试 postcss │ └── ts │ └── index.ts // 测试 ts │── babel.js │── postcss.config.js // postcss 配置 │── tsconfig.json // ts 配置 └──── dist // 打包后的目录 │── app.bundle.js │── app.css └── index.html
配置 babel
config/babelLoader.js
module.exports = (config resolve) => { const baseRule = config.module.rule('js').test(/.js│.tsx?$/); const babelPath = resolve('babel.js'); const babelConf = require(babelPath); const version = require(resolve('node_modules/@babel/core/package.json')) .version; return () => { baseRule .use('babel') .loader(require.resolve('babel-loader')) .options(babelConf({ version })); }; };
使用 babel 配置 ts
这里我们使用 babel 插件 @babel/preset-typescript 将 ts 转成 js,并使用ForkTsCheckerWebpackPlugin、ForkTsCheckerNotifierWebpackPlugin 插件进行错误提示。
babel.js
module.exports = function(api) { return { presets: [ [ '@babel/preset-env' { targets: { chrome: 59 edge: 13 firefox: 50 safari: 8 } } ] [ '@babel/preset-typescript' { allExtensions: true } ] ] plugins: [ '@babel/plugin-transform-typescript' 'transform-class-properties' '@babel/proposal-object-rest-spread' ] }; };
ts 静态类型检查
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin'); module.exports = (config resolve) => { return () => { config.plugin('ts-fork').use(ForkTsCheckerWebpackPlugin [ { // 将async设为false,可以阻止Webpack的emit以等待类型检查器/linter,并向Webpack的编译添加错误。 async: false } ]); // 将TypeScript类型检查错误以弹框提示 // 如果fork-ts-checker-webpack-plugin的async为false时可以不用 // 否则建议使用,以方便发现错误 config.plugin('ts-notifier').use(ForkTsCheckerNotifierWebpackPlugin [ { title: 'TypeScript' excludeWarnings: true skipSuccessful: true } ]); }; };
友好错误提示插件
config/FriendlyErrorsWebpackPlugin.js
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); module.exports = (config resolve) => { return () => { config.plugin('error').use(FriendlyErrorsWebpackPlugin); }; };
配置样式,style,css、less、sass、postcss 等
module.exports = (config resolve) => { const createCSSRule = (lang test loader options = {}) => { const baseRule = config.module.rule(lang).test(test); const normalRule = baseRule.oneOf('normal'); normalRule .use('extract-css-loader') .loader(require('mini-css-extract-plugin').loader) .options({ hmr: process.env.NODE_ENV === 'development' publicPath: '/' }); normalRule .use('css-loader') .loader(require.resolve('css-loader')) .options({}); normalRule.use('postcss-loader').loader(require.resolve('postcss-loader')); if (loader) { const rs = require.resolve(loader); normalRule .use(loader) .loader(rs) .options(options); } }; return () => { createCSSRule('css' /\.css$/ 'css-loader' {}); createCSSRule('less' /\.less$/ 'less-loader' {}); createCSSRule('scss' /\.scss$/ 'sass-loader' {}); createCSSRule('postcss' /\.p(ost)?css$/); }; };
postcss 配置
module.exports = { plugins: { 'postcss-px-to-viewport': { unitToConvert: 'px' viewportWidth: 750 unitPrecision: 5 propList: ['*'] viewportUnit: 'vw' fontViewportUnit: 'vw' selectorBlackList: [] minPixelValue: 1 mediaQuery: false replace: true exclude: [] landscape: false landscapeUnit: 'vw' landscapeWidth: 568 } } };
编译前后 css 对比
src/style/index.less
/* index.less */ .test { width: 300px; }
dist/app.css
/* index.css */ .test { width: 36.66667vw; height: 26.66667vw; color: red; background-color: orange; } /* app.css */ .test { font-size: 8vw; } /* index.less */ .test { width: 40vw; } /* index.scss */ .test { height: 40vw; } /* index.postcss */ .test { background: green; height: 26.66667vw; }
配置 autoprefixer
自动添加 css 前缀
postcss.config.js
module.exports = { plugins: { autoprefixer: { overrideBrowserslist: [ '> 1%' 'last 3 versions' 'iOS >= 8' 'Android >= 4' 'Chrome >= 40' ] } } };
转换前
/* index.css */ .test { width: 200px; height: 200px; color: red; display: flex; background-color: orange; }
转换后
/* index.css */ .test { width: 26.66667vw; height: 26.66667vw; color: red; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; background-color: orange; }
开启 source map
config.devtool('cheap-source-map');
└── dist │── app.bundle.js │── app.bundle.js.map │── app.css │── app.css.map └── index.html
在源文件下会有一行注释,证明开启了 sourcemap
/*# sourceMappingURL=app.css.map*/
课时 4:webpack性能优化
本章讲解
- 分离 Manifest
- Code Splitting(代码分割)
- Bundle Splitting(打包分割)
- Tree Shaking(删除死代码)
- 开启 gzip
分离 Manifest
module.exports = (config resolve) => { return () => { config .optimization .runtimeChunk({ name: "manifest" }) } }
Code Splitting
- 使用动态 import 或者 require.ensure 语法,在第一节已经讲解
- 使用 babel-plugin-import 插件按需引入一些组件库
Bundle Splitting
将公共的包提取到 chunk-vendors 里面,比如你require('vue'),webpack 会将 vue 打包进 chunk-vendors.bundle.js
module.exports = (config resolve) => { return () => { config .optimization.splitChunks({ chunks: 'async' minSize: 30000 minChunks: 1 maxAsyncRequests: 3 maxInitialRequests: 3 cacheGroups: { vendors: { name: `chunk-vendors` test: /[\\/]node_modules[\\/]/ priority: -10 chunks: 'initial' } common: { name: `chunk-common` minChunks: 2 priority: -20 chunks: 'initial' reuseExistingChunk: true } } }) config.optimization.usedExports(true) } }
Tree Shaking
config/optimization.js
config.optimization.usedExports(true);
src/treeShaking.js
export function square(x) { return x * x; } export function cube(x) { return x * x * x; }
在 main.js 中只引用了 cube
import { cube } from './treeShaking'; console.log(cube(2));
未使用 Tree Shaking
{ "./src/treeShaking.js": function( module __webpack_exports__ __webpack_require__ ) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__ "square" function() { return square; }); __webpack_require__.d(__webpack_exports__ "cube" function() { return cube; }); function square(x) { return x * x; } function cube(x) { return x * x * x; } } }
使用了 Tree Shaking
这里只导出了 cube 函数,并没有将 square 导出去
当然你可以看见 square 函数还是在 bundle 里面,但是在压缩的时候就会被干掉了,因为它并没有被引用
{ "./src/treeShaking.js": function( module __webpack_exports__ __webpack_require__ ) { "use strict"; __webpack_require__.d(__webpack_exports__ "a" function() { return cube; }); function square(x) { return x * x; } function cube(x) { return x * x * x; } } }
只有当函数给定输入后,产生相应的输出,且不修改任何外部的东西,才可以安全做shaking的操作
如何使用tree-shaking?
- 确保代码是es6格式 即 export,import
- package.json中,设置 sideEffects
- 确保 tree-shaking 的函数没有副作用
- babelrc中设置presets [["@babel/preset-env" { "modules": false }]] 禁止转换模块,交由webpack进行模块化处理
- 结合uglifyjs-webpack-plugin
其实在 webpack4 我们根本不需要做这些操作了,因为 webpack 在生产环境已经帮我们默认添加好了,开箱即用!
开启 gzip
CompressionWebpackPlugin.js
const CompressionWebpackPlugin = require('compression-webpack-plugin'); module.exports = (config resolve) => { return () => { config.plugin('CompressionWebpackPlugin').use(CompressionWebpackPlugin [ { algorithm: 'gzip' test: /\.js(\?.*)?$/i threshold: 10240 minRatio: 0.8 } ]); }; };
课时 5:手写loader实现可选链
本章内容
- 什么是 webpack loader
- 可选链介绍
- loader 实现可选链
什么是 webpack loader
webpack loader 是 webpack 为了处理各种类型文件的一个中间层,webpack 本质上就是一个 node 模块,它不能处理 js 以外的文件,那么 loader 就帮助 webpack 做了一层转换,将所有文件都转成字符串,你可以对字符串进行任意操作/修改,然后返回给 webpack 一个包含这个字符串的对象,让 webpack 进行后面的处理。如果把 webpack 当成一个垃圾工厂的话,那么 loader就是这个工厂的垃圾分类!
可选链介绍
这里并不是纯粹意义上的可选链,因为 babel 跟 ts 都已经支持了,我们也没有必要去写一个完整的可选链,只是来加深一下对 loader 的理解, loader 在工作当中能帮助我们做什么?
用途 当我们访问一个对象属性时不必担心这个对象是 undefined 而报错,导致程序不能继续向下执行
解释 在 ? 之前的所有访问链路都是合法的,不会产生报错
const obj = { foo: { bar: { baz: 2 } } } console.log(obj.foo.bar?.baz) // 2 // 被转成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz console.log(obj.foo.err?.baz) // undefined // 被转成 obj && obj.foo && obj.foo.err && obj.foo.err.baz
loader 实现可选链
配置loader,options-chain-loader
config/OptionsChainLoader.js
module.exports = (config resolve) => { const baseRule = config.module.rule('js').test(/.js|.tsx?$/); const normalRule = baseRule.oneOf('normal'); return () => { normalRule .use('options-chain') .loader(resolve('options-chain-loader')) } }
其实就是正则替换,loader 将整个文件全部转换成字符串,content 就是整个文件的内容,对 content 进行修改,修改完成后再返回一个新的 content 就完成了一个 loader 转换。是不是很简单?
下面的操作意思就是,我们匹配 obj.foo.bar?. 并把它转成 obj && obj.foo && obj.foo.bar && obj.foo.bar.
options-chain-loader.js
module.exports = function(content) { return content.replace(new RegExp(/([\$_\w\.] \?\.)/ 'g') function(res) { let str = res.replace(/\?\./ ''); let arrs = str.split('.'); let strArr = []; for(let i = 1; i <= arrs.length; i ) { strArr.push(arrs.slice(0 i).join('.')); } let compile = strArr.join('&&'); const done = compile '&&' str '.' return done; }); };
课时 6:webpack编译优化
本章内容
- cache-loader
- DllPlugin
- threadLoader
cache-loader
cache-loader 主要是将打包好的文件缓存在硬盘的一个目录里,一般存在 node_modules/.cache下,当你再次 build 的时候如果此文件没有修改就会从缓存中读取已经编译过的文件,只有有改动的才会被编译,这样就大大降低了编译的时间。尤其是项目越大时越明显。
此项目使用前后数据对比 3342ms --> 2432ms 效果还是比较明显
这里只对 babel 加入了 cache-loader,因为我们的 ts/js 都是由 babel 进行编译的,不需要对 ts-loader 缓存(我们也没有用到)
config/cacheLoader.js
module.exports = (config resolve) => { const baseRule = config.module.rule('js').test(/.js|.tsx?$/); const babelPath = resolve('babel.js') const babelConf = require(babelPath); const version = require(resolve('node_modules/@babel/core/package.json')).version return () => { baseRule .exclude .add(filepath => { // 不缓存 node_modules 下的文件 return /node_modules/.test(filepath) }) .end() .use('cache-loader') .loader('cache-loader') .options({ // 缓存位置 cacheDirectory: resolve('node_modules/.cache/babel') }) } }
DllPlugin
DllPlugin 是将第三方长期不变的包与实际项目隔离开来并分别打包,当我们 build 时再将已经打包好的 dll 包引进来就 ok 了
我提取了两个包 vue、react,速度差不多提升了 200ms,从 2698ms 到 2377ms
打包 dll
build/dll.js
const path = require("path"); const dllPath = path.join(process.cwd() 'dll'); const Config = require('webpack-chain'); const config = new Config(); const webpack = require('webpack') const rimraf = require('rimraf'); const ora = require('ora') const chalk = require('chalk') const BundleAnalyzerPlugin = require('../config/BundleAnalyzerPlugin')(config) BundleAnalyzerPlugin() config .entry('dll') .add('vue') .add('react') .end() .set('mode' "production") .output .path(dllPath) .filename('[name].js') .library("[name]") .end() .plugin('DllPlugin') .use(webpack.DllPlugin [{ name: "[name]" path: path.join(process.cwd() 'dll' 'manifest.json') }]) .end() rimraf.sync(path.join(process.cwd() 'dll')) const spinner = ora('开始构建项目...') spinner.start() webpack(config.toConfig() function (err stats) { spinner.stop() if (err) throw err process.stdout.write(stats.toString({ colors: true modules: false children: false chunks: false chunkModules: false }) '\n\n') if (stats.hasErrors()) { console.log(chalk.red('构建失败\n')) process.exit(1) } console.log(chalk.cyan('build完成\n')) })
将 dll 包合并
const webpack = require('webpack') module.exports = (config resolve) => { return () => { config.plugin('DllPlugin') .use(webpack.DllReferencePlugin [{ context: process.cwd() manifest: require(resolve('dll/manifest.json')) }]) } }
threadLoader
测试效果变差了 ,线程数越小编译速度越快
config/threadLoader.js
module.exports = (config resolve) => { const baseRule = config.module.rule('js').test(/.js|.tsx?$/); return () => { const useThreads = true; if (useThreads) { const threadLoaderConfig = baseRule .use('thread-loader') .loader('thread-loader'); threadLoaderConfig.options({ workers: 3 }) } } }
课时 7:多页面配置
注意
- 弃用 npm run build & npm run dev & npm run dll
- 改成 box build & box dev & box dll
- link npm link 将 box 命令链接到全局
本章内容
- 使用
- 改造为脚手架
- 多页面配置
使用
box build # 不加参数则会编译所有页面,并清空 dist box dev # 默认编译 index 页面
参数
# index2 是指定编译的页面。不会清空 dist # report 开启打包分析 box build index2 --report box dev index2 --report
改造为脚手架
分成三个命令,进行不同操作
- build
- dev
- dll
bin/box.js
#!/usr/bin/env node const chalk = require('chalk') const program = require('commander') const packageConfig = require('../package.json'); const { cleanArgs } = require('../lib') const path = require('path') const __name__ = `build dev dll` let boxConf = {} let lock = false try { boxConf = require(path.join(process.cwd() 'box.config.js'))() } catch (error) { } program .usage('<command> [options]') .version(packageConfig.version) .command('build [app-page]') .description(`构建开发环境`) .option('-r --report' '打包分析报告') .option('-d --dll' '合并差分包') .action(async (name cmd) => { const options = cleanArgs(cmd) const args = Object.assign(options { name } boxConf) if (lock) return lock = true; if (boxConf.pages) { Object.keys(boxConf.pages).forEach(page => { args.name = page; require('../build/build')(args) }) } else { require('../build/build')(args) } }) program .usage('<command> [options]') .version(packageConfig.version) .command('dev [app-page]') .description(`构建生产环境`) .option('-d --dll' '合并差分包') .action(async (name cmd) => { const options = cleanArgs(cmd) const args = Object.assign(options { name } boxConf) if (lock) return lock = true; require('../build/dev')(args) }) program .usage('<command> [options]') .version(packageConfig.version) .command('dll [app-page]') .description(`编译差分包`) .action(async (name cmd) => { const options = cleanArgs(cmd) const args = Object.assign(options { name } boxConf) if (lock) return lock = true; require('../build/dll')(args) }) program.parse(process.argv).args && program.parse(process.argv).args[0]; program.commands.forEach(c => c.on('--help' () => console.log())) if (process.argv[2] && !__name__.includes(process.argv[2])) { console.log() console.log(chalk.red(` 没有找到 ${process.argv[2]} 命令`)) console.log() program.help() } if (!process.argv[2]) { program.help() }
多页面配置
box.config.js
module.exports = function (config) { return { entry: 'src/main.js' // 默认入口 dist: 'dist' // 默认打包目录 publicPath: '/' port: 8888 pages: { index: { entry: 'src/main.js' template: 'public/index.html' filename: 'index.html' } index2: { entry: 'src/main.js' template: 'public/index2.html' filename: 'index2.html' } } chainWebpack(config) { } } }
课时 8:手写一个webpack插件
如果把 webpack 当成一个垃圾工厂,loader 就是垃圾分类,将所有垃圾整理好交给 webpack。plugin 就是如何去处理这些垃圾。
webpack 插件写起来很简单,就是你要知道各种各样的钩子在什么时候触发,然后你的逻辑写在钩子里面就ok了
- apply 函数是 webpack 在调用 plugin 的时候执行的,你可以认为它是入口
- compiler 暴露了和 webpack 整个生命周期相关的钩子
- Compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
本节概要
- 实现一个 CopyPlugin
- 使用
实现一个 CopyPlugin
我们今天写一个 copy 的插件,在webpack构建完成之后,将目标目录下的文件 copy 到另一个目录下
const fs = require('fs-extra') const globby = require('globby') class CopyDirWebpackPlugin { constructor(options) { this.options = options; } apply(compiler) { const opt = this.options compiler.plugin('done' (stats) => { if (process.env.NODE_ENV === 'production') { (async ()=>{ const toFilesPath = await globby([`${opt.to}/**` '!.git/**']) toFilesPath.forEach(filePath => fs.removeSync(filePath)) const fromFilesPath = await globby([`${opt.from}/**`]) fromFilesPath.forEach(fromPath => { const cachePath = fromPath fromPath = fromPath.replace('dist' opt.to) const dirpaths = fromPath.substring(0 fromPath.lastIndexOf('/')) fs.mkdirpSync(dirpaths) fs.copySync(cachePath fromPath) }) console.log(` 完成copy ${opt.from} to ${opt.to}`) })() } }); } } module.exports = CopyDirWebpackPlugin
使用
将打包出来的 dist 目录下的内容 copy 到 dist2 目录下
const CopyPlugin = require('../webapck-plugin-copy'); module.exports = ({ config }) => { return () => { config.plugin('copy-dist') .use(CopyPlugin [{ from: 'dist' to: 'dist2' }]) } }
课时 9:构建 ssr
ssr 就是服务端渲染,做 ssr 的好处就是为了处理 spa 的不足,比如 seo 优化,服务端缓存等问题。
今天主要用 react 的 ssr 来做一个简单的实例,让大家更清晰的入门
本章概要
- 创建 box build:ssr
- 编译 ssr
- 编译 jsx 语法
- 入口区分服务端/客户端
- 服务端渲染
- 小结
创建 box build:ssr
老规矩,先来一个 box build:ssr 命令让程序可以执行
执行 box build:ssr 会调用 build/ssr 执行编译
program .usage('<command> [options]') .version(packageConfig.version) .command('build:ssr [app-page]') .description(`服务端渲染`) .action(async (name cmd) => { const options = cleanArgs(cmd); const args = Object.assign(options { name } boxConf); if (lock) return; lock = true; require('../build/ssr')(args); });
编译 ssr
与其他的编译没有什么区别,值得住的是
- target 指定为 umd 模式
- globalObject 为 this
- 入口改为 ssr.jsx
.libraryTarget('umd') .globalObject('this')
build/ssr.js
module.exports = function(options) { const path = require('path'); const Config = require('webpack-chain'); const config = new Config(); const webpack = require('webpack'); const rimraf = require('rimraf'); const ora = require('ora'); const chalk = require('chalk'); const PATHS = { build: path.join(process.cwd() 'static') ssrDemo: path.join(process.cwd() 'src' 'ssr.jsx') }; require('../config/babelLoader')({ config tsx: true })(); require('../config/HtmlWebpackPlugin')({ config options: { publicPath: '/' filename: 'client.ssr.html' } })(); config .entry('ssr') .add(PATHS.ssrDemo) .end() .set('mode' 'development') // production .output.path(PATHS.build) .filename('[name].js') .libraryTarget('umd') .globalObject('this') .library('[name]') .end(); rimraf.sync(path.join(process.cwd() PATHS.build)); const spinner = ora('开始构建项目...'); spinner.start(); webpack(config.toConfig() function(err stats) { spinner.stop(); if (err) throw err; process.stdout.write( stats.toString({ colors: true modules: false children: false chunks: false chunkModules: false }) '\n\n' ); if (stats.hasErrors()) { console.log(chalk.red('构建失败\n')); process.exit(1); } console.log(chalk.cyan('build完成\n')); }); };
编译 jsx 语法
因为我们是用 react 写的,避免不了会用到 jsx 语法,所以我们需要在 babel-loader 中使用 @babel/preset-react
npm i @babel/preset-react -D
config/babelLoader.js
if (tsx) { babelConf.presets.push('@babel/preset-react'); }
入口区分服务端/客户端
区分服务端跟客户端分别渲染
const React = require("react"); const ReactDOM = require("react-dom"); const SSR = <div onClick={() => alert("hello")}>Hello world</div>; if (typeof document === "undefined") { console.log('在服务端渲染') module.exports = SSR; } else { console.log('在客户端渲染') const renderMethod = !module.hot ? ReactDOM.render : ReactDOM.hydrate; renderMethod(SSR document.getElementById("app")); }
服务端渲染
- 将打包出来的 static 文件夹作为一个服务
- 访问 http://127.0.0.1:8080,进入服务端渲染的页面
- 再执行一遍 ssr.js 进行事件绑定
module.exports = function (options) { const express = require("express"); const { renderToString } = require("react-dom/server"); const chalk = require('chalk') const SSR = require("../static/ssr"); const port = process.env.PORT || 8080; server(port); function server(port) { const app = express(); app.use(express.static("static")); app.get("/" (req res) => res.status(200).send(renderMarkup(renderToString(SSR))) ); const empty = ' ' const common = `App running at: - Local: http://127.0.0.1:${port}\n` console.log(chalk.cyan('\n' empty common)) app.listen(port () => process.send && process.send("online")); } function renderMarkup(html) { return `<!DOCTYPE html> <html> <head> <title>Webpack SSR Demo</title> <meta charset="utf-8" /> </head> <body> <div id="app">${html}</div> <script src="./ssr.js"></script> </body> </html>`; } }
小结
至此 ssr 已经结束了,其实所有看起来很高大上的技术都是从一点一滴积累起来的,只要我们明白原理,你也能做出更优秀的框架
本文作者:前端技匠
原文链接:https://juejin.im/post/5de06aa851882572d672c1ad