概念
Webpack是一个打包模块化JavaScript的工具,它会从入口模块出发,识别出源码中的模块化导入语句,递归的找出入口文件的所有依赖,将入口和其所有的依赖打包到一个单独的文件中。
安装
推荐局部安装- 局部安装:
不推荐全局安装会造成版本指定,如果多个项目依赖的版本不同,会造成构建失败。
yarn add webpackyarn add webpack-cli
- 检查版本
npx webpack -v - 指定版本安装
yarn add webpack@xxx.xx - 启动
package.json "script": {"dev": "webpack"} yarn run dev
默认配置
- 默认入口 :
./src/index.js 默认输出 :
./dist 名称:main.js// webpack.config.js(默认命名)中的配置会覆盖webpack的默认配置const path = require('path');module.exports = {// webpack执行构建入口entry: "./src/index.js",output: {// 将所有依赖的模块合并输出到main.jsfilename: "main.js",// 输出文件的存放路径,必须是绝对路径// 根路径是config所在目录path: path.resolve(__dirname, "./dist")}};
自定义
webpack配置,package.json ---> "script": {"run name": "webpack --config webpack filename"}webpack默认支持多种模块规范,如commonJS、ES module AMDwebpack默认支持js模块、json模块
核心概念
1. module
// 主要对文件类型解析的loader配置// ...modules: {rules: [{test: /\...$/, use: ["file-loader"]},// loader具有执行顺序,自右往左{test: /\.css$/, use: ["style-loader", "css-loader", "postcss-loader"]}// ...]}
拓展:css前缀自动填充
// postcss.config.jsmodule.exports = {plugins: [require('autoprefixer')({// 兼容最近两个版本, 市场占有率大于1%overrideBrowserslist: ['last 2 versions', '>1%']})]}
2. plugins
html-webpack-plugin// html-webpack-pluginconst HtmlWebpackPlugin = require('html-webpack-plugin');//...plugins: [new HtmlWebpackPlugin({template: '', // 目标模板filename: '', // 编译后的模板名chunks: [] // 模板引入的主入口}),//...]
clean-webpack-plugin// clean-webpack-plugin 清除编译文件夹中冗余文件const { CleanWebpackPlugin } = require('clean-webpack-plugin');//...plugins: [// 插件以config所在目录为根目录new CleanWebpackPlugin(['dist'], {// 指定config配置的上一级为根路径,此种配置为了解决类似vue将config集合到build文件夹情况root: path.resolve(__dirname, '../')}),//...]
mini-css-extract-plugin// mini-css-extract-plugin 整合css文件const MiniCssExtractPlugin = require('mini-css-extract-plugin');//...module: {rules: [{test: /\.css$/,// 使用MiniCssExtractPlugin的loader代替style-loaderuse: [MiniCssExtractPlugin.loader, 'css-loader', ...]}]},plugins: [new MiniCssExtractPlugin({filename: '[name][hash:8].css' // 编译后css文件名}),]
3. dev-tool
// dev-tool 设置对应属性值,控制编译后是否生成map文件//...devtool:'none', // 不生成,production环境'source-map', // 生成map文件'eval-source-map',//....
4. webpack-dev-server
- 提升开发效率
每次修改代码需要重新打包一次,打开浏览器然后刷新,安装使用webpackDevServer进行配置,可以解决以上问题。
- 安装
npm install webpack-dev-server -D - 配置 ```javascript // package.json “scripts”: { “server”: “webpack-dev-server” }
// webpack.config.js devServer: { contentBase: ‘’, // 定位文件位置 open: true, // 自动打开浏览器 port: … // 端口号 proxy: { // 跨域代理 “/api”: { // 解析分割路径/api target: “http://localhost:8888“ } } }
<a name="7fd0ef0f"></a>#### 5. `Hot Module Replacement(HMR: 热模块替换)`- `webpack`自带插件- ```javascript// webpack.config.jsconst webpack = require('webpack');devServer: {hotOnly: true, // 禁止刷新页面}// ...plugins: [new webpack.HotModuleReplacementPlugin(),]
CSS样式HMR只支持css的style-loader处理方式,不支持抽离成独立文件的方式(mini-css-extract-plugin)
JSJS模块,HMR需要手动监听需要HMR的模块if(module.hot) {moudle.hot.accept('监听的模块', function(){...});}
Babel
Babel是JavaScript编译器,能将ES6代码转换成ES5代码,让我们开发过程中放心使用JS新特性而不用担心兼容问题。并且还可以通过插件机制根据需求灵活的扩展。
Babel在执行编译的过程中,会从项目根目录下的.babelrc JSON文件中读取配置,没有该文件会从loader的options地方读取配置。
安装
npm i babel-loader @babel/core @babel/preset-env -D
ES6配置
babel-loader是webpack与babel的通信桥梁,@babel/preset-env里包含了ES6 7 8转换为ES5的规则,类似Promise等语法仍需要特殊处理,使用@babel/polyfillbabel—-> 分析依赖 —->AST(抽象语法树) —-> 通过babel-preset中的规则转换 —-> 生成代码
// webpack.config.js{test: /\.js$/,exclude: /node_modiles/,loader: 'babel-loader',options: {presets: [['@babel/preset-env', {targets: {edge: '17',firefox: '60',chrome: '67',safari: '11.1'}, // 浏览器支持版本corejs: 2, // 新版本需要指定核心库版本useBuiltIns: 'usage' // 按需注入}]]}}// useBuiltIns是babel7的新功能,这个选项告诉babel如何配置@babel/polyfill// 参数: entry usage false// entry: 需要在webpack的入口文件里import "@babel/polyfill"一次,babel会根据你的使用情况导入垫片,没有使用的功能不会被导入相应的垫片。// usage: 不需要import, 全自动检测, 但是要安装@babel/polyfill。// false: 如果import @babel/polyfill, 整个polyfill库引入项目中,导致打包体积很大。
React配置
安装
@babel/preset-react
{test: /\.js$/,exclude: /node_modiles/,loader: 'babel-loader',options: {presets: [['@babel/preset-env'],'@babel/preset-react']}}
单页面(SPA)多入口打包
// webpack.config.js{entry: {index: './src/index.js',list: './src/list.js',other: './src/other.js'},output: {filename: '[name].js',path: path.resolve(__dirname, './build')},//...plugins: [new htmlWebpackPlugin({template: './src/index.html',filename: 'index.html',chunks: ['index'] // 打包编译后,html引入对应名的js文件}),new htmlWebpackPlugin({template: './src/list.html',filename: 'list.html',chunks: ['list']}),new htmlWebpackPlugin({template: './src/other.html',filename: 'other.html',chunks: ['other']}),]}
MPA打包
const glob = require('glob');const setMap = () => {const entry = {};const htmlwebpackplugin = [];//! 分析入口文件路径const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));entryFiles.map((item, index) => {const entryFile = entryFiles[index];//! 过滤信息拿到入口名称const match = entryFile.match(/src\/(.*)\/index\.js/);const pageName = match && match[1];entry[pageName] = entryFile;//! 配置htmlWebpackPluginhtmlwebpackplugin.push(new htmlWebpackPlugin({template: 'src/index.html',filename: `${pageName}.html`,chunks: [pageName]}))})return {entry,htmlwebpackplugin}}const { entry, htmlwebpackplugin } = setMap();
自定义实现webpack打包构建功能
webpack.js
/*** * 获取配置,根据配置信息启动webpack,执行构建* * 1. 从入口模块开始分析 //? 1.1 有哪些依赖 1.2 转换文件* * 2. 递归的分析其他依赖模块 //? 2.1 有哪些依赖 2.2 转换文件* * 3. 生成可以在浏览器端执行的bundle文件**///! 启动webpack node webpack.js// webpack.config.js 会返回一个对象,包含各种配置const options = require('./webpack.config.js');const Compiler = require('./lib/compile.js');new Compiler(options).run();
lib/compiler.js
const { getAst, getDependencies, getCode } = require('./parser.js');const path = require('path');const fs = require('fs');class Compiler {constructor(options){this.entry = options.entry;this.output = options.output;this.modules = [];}run() {const info = this.build(this.entry);this.modules.push(info);for (let i = 0; i < this.modules.length; i++) {const { dependencies } = this.modules[i];if(dependencies) {for (let item in dependencies) {this.modules.push(this.build(dependencies[item]));}}}// 转换数据结构let obj = {};this.modules.forEach(item => {obj[item.fileName] = {dependencies: item.dependencies,code: item.code}});console.log(obj);// 生成代码文件this.file(obj);}build(fileName) {// 1. 分析入口,读取入口模块的内容// 2. 读取的内容通过babel/parser解析成AST抽象语法树let ast = getAst(fileName);// 3. 使用babel/traverse对AST进行解析,获取入口文件依赖的相对路径let dependencies = getDependencies(ast, fileName);// 4. 使用babel/core中的transformFromAst对AST进行解析转换let code = getCode(ast);return {fileName,dependencies,code}}file(code) {// 获取输出信息const filePath = path.join(this.output.path, this.output.fileName);const newCode = JSON.stringify(code);/**{'./src/index.js': {dependencies: {'./Hello.js': 'src\\Hello.js'},code: '"use strict";\n\nvar _Hello = require("./Hello.js");\n\nconsole.log(\'hello\' + (0, _Hello.say)(\'你好\'));'},'src\\Hello.js': {dependencies: {},code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say= say;\n\nfunction say(key) {\n return key;\n}'}}*/// 传入解析后的代码文件// 根据入口路径进行代码引入并执行const bundle = `(function(graph){function require(module) {function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);}var exports = {};// 执行code时,如果遇到require,会递归调用localRequire,返回引入的文件(function(require, exports, code){eval(code);})(localRequire, exports, graph[module].code);return exports;}// 入口路径require('${this.entry}')})(${newCode})`;fs.writeFileSync(filePath, bundle, 'utf-8');}}module.exports = Compiler;
lib/parser.js
const parser = require('@babel/parser');const traverse = require('@babel/traverse').default;const {transformFromAst} = require('@babel/core');const fs = require('fs');const path = require('path');module.exports = {// 分析模块 获取ast抽象语法树getAst: fileName => {let content = fs.readFileSync(fileName, 'utf-8');return parser.parse(content, {sourceType: 'module'})},// 获取依赖项路径对象集getDependencies: (ast, fileName) => {// ! 可以保留相对路径和绝对路径let dependencies = {};traverse(ast, {ImportDeclaration({node}) {const dirname = path.dirname(fileName);const newPath = path.join(dirname, node.source.value);dependencies[node.source.value] = newPath;}});return dependencies;},// 获取ast解析后转换的文件信息输出getCode: ast => {const { code } = transformFromAst(ast, null, {presets: [ require('@babel/preset-env') ]});return code; // '"use strict";\n\nvar _Hello = require("./Hello.js");\n\nconsole.log(\'hello\' + (0, _Hello.say)(\'你好\'));'},}
自定义Loader
// webpack.config.js//...省略module: {rules: [{test: ...,loader: path.resolve(__dirname, '...'), // 绝对路径options: {...} // 在自定义loader中可以使用query获取到options内容,也可以使用loader-utils工具集}]}// 自定义loadermodule.exports = function selfLoader(source) {source // 调用该loader时,传入的资源文件this // node对象return ... // 必须有返回值}
loader多个返回值
// 自定义loadermodule.exports = function selfLoader(source) {// 返回多个信息,可以使用this.callback// this.callback(// err: Error | null,// content: string | Buffer,// sourceMap?: SourceMap,// meta?: any// );return this.callback(null, str);}
loader的异步操作
// loader中有异步操作,使用this.async声明为异步操作 结果值使用callback进行返回。const callback = this.async();// ....return callback(null, str);
多个loader
// 自定义loader可以进行配置,从而只需写loader文件名也可被查找到resolveLoader: ['node_modules', './loaders'];// 多个loader引入,如果有先后关系,按照从右向左顺序引入
自定义Plugin
基本结构
class CopyrightWebpackPlugin {constructor(){}// compiler: webpack实例apply(compiler){}}module.exports = CopyrightWebpackPlugin;
compiler的hooks函数
class CopyrightWebpackPlugin {constructor(options){}// compiler webpack实例apply(compiler){// 在输出到目标文件夹之前进行异步写入,同步是tap,异步是tapAsynccompiler.hooks.emit.tapAsync("CopyrightWebpackPlugin",(compilation, cb) => {compilation.assets["deankwan.txt"] = {// 文件内容source: function() {return "dean kwan";},// 文件大小size: function() {return 20;}}cb();})}}module.exports = CopyrightWebpackPlugin;
