webpack
核心概念
一切静态资源视为模块,又叫静态资源模块打包器。
通过入口文件递归构建依赖图,构建抽象语法树(如没有引用的东西就不会被找到),借助不同 loader 处理相应的文件源码,最终输出目标环境(浏览器)可执行的代码。
通常使用其构建项目时,维护的是一份配置文件(webpack.config),如果整个 webpack 视为一个函数,那么这份配置就是函数的参数,通过修改参数来控制输出的结果。
借助于 loader/plugin 可以差异化处理不同的文件类型。如有个性化需求,还可以实现自定的 loader/plugin
webpack 从零到一
- 初始化目录
yarn init
- 安装webpack webpack-cli
yarn add webpack webpack-cli -D
- 创建相关文件 index.html index.js
<!-- index.html -->
<html>
<head>
<meta charset="utf-8">
<title>webpack</title>
</head>
<body>
<div id="app"></div>
</body>
<script type="text/javascript" src="./output/main.js"></script>
<!-- // output/main.js是打包后的js代码路径 -->
</html>
// index.js
function test(content) {
document.querySelector('#app').innerHTML = content;
}
test('something');
在不关联的情况下通过 webpack 打包:配置package.json
"scripts": {
"build": "webpack ./src/index.js -o ./output --mode=development --devtool=cheap-module-source-map"
},
其中:
webpack: 使用
./src/index.js: 读取 index 文件
-o ./output: 输出目录为output
--mode=development: 防止压缩,启用开发环境
--devtool=cheap-module-source-map: 与源码进行关联(默认eval()源码为字符串)参考下面 source-map 干嘛地
产出文件: output main.js 期望产出文件打印 #app 的content
产物分析:外层加了闭包,防止变量被外部访问
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
function test(content) {
document.querySelector('#app').innerHTML = content;
}
test('something');
/******/ })()
;
//# sourceMappingURL=main.js.map
面试题:loader 和 plugins 有什么区别?
- loader 针对不同的文件类型进行处理,局限于一个类型的文件,写的时候需要匹配文件后缀。
- plugins 对于webpack 整个实例周期都生效,更新、打包都不分文件类型。它对 webpack 的钩子起作用,比如编译的时候或者编译完成的时候执行。
webpack 从一到二
-
新建 webpack.config.js
const path = require('path'); module.exports = { mode: 'development', devtool: 'cheap-module-source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'output'), filename: 'main.js' // 默认名字 main.js }, } // 对比 build 命令 // ./src/index.js -o ./output --mode=development --devtool=cheap-module-source-map"此时,直接在package.json 中 build 后面的代码相当于写在 config.js 中了
"scripts": { "build": "webpack" },source-map 干什么用的?
使用 source-map index.js打断点,main.js 给出源码
默认使用 eval
源码与产物的映射关系,有助于打断点找错误
-
增加es6的转换能力 添加 src/es6.js
export default class CountChange { count = 1 increment = () => { this.count++ } decrease = () => { this.count--; } } // index.js 中引入index.js 做一些修改
const instance = new CountChange(); function test(content) { document.querySelector('#app').innerHTML = content; } test(instance.count)产物分析:并没有进行 es 降级
__webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (/* binding */ CountChange) /* harmony export */ }); class CountChange { count = 1 increment () { this.count++ } decrease () { this.count--; } }安装babel!!!
yarn add @babel/core @babel/preset-env babel-loader -D增加配置
module: { rules: [{ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'] ], } } }] }再次打包,产物
var CountChange = /*#__PURE__*/_createClass(function CountChange() { var _this = this; _classCallCheck(this, CountChange); _defineProperty(this, "count", 1); _defineProperty(this, "increment", function () { _this.count++; }); _defineProperty(this, "decrease", function () { _this.count--; }); }); -
增加装饰器 装饰器:类方法其增强作用的高阶函数,不修改类本身
// es6.js const decorator = (target, key, descriptor) => { target[key] = function (...args) { console.log(this.count); return descriptor.value.apply(this, args); }; return target[key]; } export default class CountChange { count = 1 @decorator ... }此时编译会报错,因为@不识别
安装相应插件:
yarn add @babel/plugin-proposal-decorators -D加上相应的plugin
//config.js preset: [...], plugins: [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ]产物分析:
(_descriptor = _applyDecoratedDescriptor(_class.prototype, "increment", [decorator], { configurable: true, enumerable: true, writable: true,验证装饰器:
// index.js setInterval(() => { instance.increment() test(instance.count) }, 1000) -
用于生产的react脚手架 react 在函数口是render函数
index.js //render(<App />, $el)render 来自react-dom 包,并且需要编译
安装
yarn add react react-dom @babel/preset-react -S配置
// config.js presets: [ '@babel/preset-env', '@babel/preset-react' ],-D 和 -S 区别: -D 依赖都在开发环境下,打包的时候不回出现在产物里 -S 线上和开发都会有,打包之后会出现在产物里
修改index.js
import React from 'react'; import { render } from 'react-dom'; const App = () => <div>App</div>; render(<App />, document.querySelector('#app'));产物分析:
var App = function App() { return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement("div", null, "App"); }; (0,react_dom__WEBPACK_IMPORTED_MODULE_1__.render)( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(App, null), document.querySelector('#app')); })(); /******/ })() ; //# sourceMappingURL=main.js.map -
缓存包提取
// config.js module: {...}, optimization: { splitChunks: { cacheGroups: { vendor: { filename: 'vendor.js', chunks: 'all', // async 打异步的包,initial 同步的包,不区分 为 all test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/ }, // 两个包领出来,不要出现在main中 } } }产物分析,多出一个vendor 其实就是领出来的东西,这里是react-dom 和 react
记得在index.html 中引入 vendor!!
问题vendor 是干嘛的?
在开发的时候某些依赖组是不变的,发布之后代码对于用户而言不用每次都更新,直接一个强缓存,用户下载一次就不需要再下载了 因此在开发的时候将这部分代码剔除出去
问题:vendor 过大了怎么办?
可以使用 externals 相当于远程使用 cdn
output: {...}, externals: { 'react': 'React' // 相当于 import React from 'react' => const React = window.React => src="react.cdn" } -
css loader
- 添加一个css文件
#app { color: red; }直接打包会报错
ERROR in ./src/style.css 1:0 Module parse failed: Unexpected token- 安装 css loader
yarn add style-loader css-loader -D- 配置,加一个rules
// config.js rules: [{...}, { test: /\.css$/, use: ['style-loader', 'css-loader'] }]css-loader 是处理文件的 style-loader 是向页面注入 style 标签的
-
样式抽离 样式插件
- 安装
yarn add mini-css-extract-plugin -D- 导入! 插件的特点是必须导入,loader 不用导入
// config.js const MiniCssExtractPlugin = require("mini-css-extract-plugin");- 配置
// config.js module: {...}, plugins: [ new MiniCssExtractPlugin({ filename: "[name].css", chunkFilename: "[id].css" }) ],需要切换style-loader,因为style-loader 是将样式作为style标签注入页面的,现在不需要了
// config.js rules: [{...}, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }]产出了样式表,但是没有引入。又需要手动去引入,显然,一次次手动不符合开发情景
-
自动引入脚本和样式表
yarn add html-webpack-plugin -D安装 --> 引入 --> 配置
// config.js plugins: [ new HtmlWebpackPlugin({ template: './index.html' }), new MiniCssExtractPlugin({...})处理图片/自定义文件方法相同
-
热更新 HMR -- 类似ajax 异步更新 目前为止,我们的操作为:打包+刷新 模式查看代码效果。可以借助本地开发服务器来解决这个问题
- 安装
yarn add webpack-dev-server -D- package.json 中添加命令:
// package.json "scripts": { "build": "webpack", "start": "webpack serve" },开发模式的包在内存中,不输出文件 memory.fs
目前修改 App 组件,页面实时更新了,只不过还是【刷新】
修改一下 devServer 配置,顺便改一下端口
//webpack.config.js entries: ..., devSever: { port: 8000, hot: true },还是在【刷新】,更新需要注册一个回调:开启热更新会监听根组件
//index.js if (module.hot) { module.hot.accept(App, () => { render(<App />, document.querySelector('#app')); }); }或者可以写成import 形式
// index.js import App from './App' if (module.hot) { module.hot.accept('./App', () => { // 第一个参数为路径 render(<App />, document.querySelector('#app')); }); }根节点只要有更新了,就会执行回调 产出:页面保存后不会出现刷新页面的情况了,直接改变
异步组件打包和代码热更新
- 异步组件 react lazy
//index.js
const lazy = fn => class extends React.Component {
state = {
Component: () => null
}
// 默认加载的时候为一个 loading 图片
async componentDidMount() {
const { default: Component } = await fn();
this.setState({ Component });
}
// 异步加载数据
render() {
const Component = this.state.Component;
return <Component {...this.props} />;
}
// 返回真是的组件
}
// 声明一个异步组件,用lazy 包裹导入组件
const Async = lazy(() => import('./Async'));
// 指定产出的模块名称(注意这里的注释是有用的):
const Async = lazy(() => import(/* webpackChunkName: "Async" */ './Async'));
写出Async 组件
// src/Async
import React from 'react'
export default function Async() {
return (
<div>
Async
</div>
);
}
导入后放在App 后面
// index.js
const App = () => <div>App <Async /></div>;
报错:Uncaught ReferenceError: regeneratorRuntime is not defined
原因:babel默认只转换新的JavaScript语法(syntax),如箭头函数等,而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,此时需要一些辅助函数(babel 6.x以下版本借助polyfill,需要在entry之前或根文件头部引入,本课程均以babel 7之后的标准讲解)
现在解决 直接安装regeneratorRuntime
yarn add @babel/plugin-transform-runtime -D
配置:
// config.js
// 在babel 的 plugins里
plugins: [
"@babel/plugin-transform-runtime",
["@babel/plugin-proposal-decorators", { "legacy": true }],
]
output 加hash
output: {
path: path.resolve(__dirname, 'output'),
filename: '[name].[hash:6].js' // 默认名字 main.js
},
xxxxx require.ensure 异步路由 webpack 支持的书写形式
getComponent () {
require.ensure(_, () => {
const ensure = require('./requireEnsure').default;
cb(ensure);
}, err, 'Home')
}
import 和 require 的区别
- import 静态导入语法:必须在顶级作用域中写,不能在条件中使用,在代码执行前,就是知道所有依赖内容
- require 可以在函数或者事件内部写,因此执行中会出现问题