├── .prettierignore ├── .eslintignore ├── .gitignore ├── src ├── templates │ ├── basic │ │ ├── hello.js │ │ ├── index.js │ │ ├── utils.js │ │ └── async_import.js │ └── loaders │ │ ├── test-my.js │ │ ├── test-css.js │ │ ├── test-css.css │ │ └── test.my ├── loaders │ └── my-loader.js ├── examples │ ├── webpack.basic.js │ ├── webpack.hello-plugin.js │ ├── webpack.multiple-entries.js │ ├── webpack.my.js │ ├── webpack.async.js │ └── webpack.css.js └── plugins │ └── hello-plugin.js ├── .huskyrc.js ├── .lintstagedrc.js ├── .editorconfig ├── README.md ├── .vscode └── settings.json ├── .eslintrc.js ├── .commitlintrc.js ├── LICENSE ├── .prettierrc.js ├── package.json └── docs ├── Webpack模块化原理.md ├── Webpack动态导入原理.md ├── Webpack模块联邦原理.md └── WebpackLoader原理学习之css-loader.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | src/templates/basic/async_import.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | dist/ 5 | 6 | *.log 7 | 8 | !.vscode/ -------------------------------------------------------------------------------- /src/templates/basic/hello.js: -------------------------------------------------------------------------------- 1 | export default function(name) { 2 | console.log(`hello ${name}`); 3 | } 4 | -------------------------------------------------------------------------------- /src/templates/loaders/test-my.js: -------------------------------------------------------------------------------- 1 | import data from './test.my?default=12'; 2 | 3 | console.log(data); 4 | -------------------------------------------------------------------------------- /src/templates/basic/index.js: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | 3 | const result = utils.add(1, 2); 4 | console.log(result); 5 | -------------------------------------------------------------------------------- /src/templates/loaders/test-css.js: -------------------------------------------------------------------------------- 1 | import style from './test.css'; 2 | console.log(style); 3 | console.log(style.toString()); 4 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | 'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS', 4 | 'pre-commit': 'lint-staged' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/templates/loaders/test-css.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background: #ccc; 4 | height: 100vh; 5 | } 6 | 7 | h1 { 8 | font-size: 100px; 9 | } 10 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.js': ['eslint --fix'], 3 | '*.{md,html,json}': ['prettier --write'], 4 | '*.{css,scss,less}': ['prettier --write'] 5 | }; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/templates/basic/utils.js: -------------------------------------------------------------------------------- 1 | export const add = (x, y) => x + y; 2 | export const num = 10; 3 | export const obj = { a: { b: 1 } }; 4 | 5 | export default { 6 | add, 7 | num, 8 | obj 9 | }; 10 | -------------------------------------------------------------------------------- /src/templates/basic/async_import.js: -------------------------------------------------------------------------------- 1 | setTimeout(async () => { 2 | const utils = await import(/* webpackChunkName: "utils" */ './utils'); 3 | const hello = await import(/* webpackChunkName: "hello" */ './hello'); 4 | console.log(utils); 5 | console.log(hello); 6 | }, 3000) -------------------------------------------------------------------------------- /src/loaders/my-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function(content) { 2 | const data = JSON.parse(content); 3 | if (data.code === 200) { 4 | const list = data.data; 5 | return `export default ${JSON.stringify(list)}`; 6 | } 7 | return `export default undefined`; 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack 学习笔记 2 | 3 | ## [1. Webpack 模块化原理](./docs/Webpack模块化原理.md) 4 | 5 | ## [2. Webpack 动态导入原理](./docs/Webpack动态导入原理.md) 6 | 7 | ## [3. Webpack Loader 原理学习之 css-loader](./docs/WebpackLoader原理学习之css-loader.md) 8 | 9 | ## [3. Webpack5 模块联邦原理](./docs/Webpack模块联邦原理.md) 10 | -------------------------------------------------------------------------------- /src/examples/webpack.basic.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: path.join(__dirname, '../templates/basic/index.js'), 6 | output: { 7 | path: path.join(__dirname, '../../dist'), 8 | filename: 'bundle.js' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.format.enable": true, 4 | "eslint.lintTask.enable": true, 5 | "eslint.validate": ["javascript"], 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "[javascript]": { 8 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/examples/webpack.hello-plugin.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HelloPlugin = require('../plugins/hello-plugin.js'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.join(__dirname, '../templates/basic/index.js'), 7 | output: { 8 | path: path.join(__dirname, '../../dist'), 9 | filename: 'bundle.js' 10 | }, 11 | plugins: [ 12 | new HelloPlugin({ 13 | flag: true 14 | }) 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /src/templates/loaders/test.my: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "data": [ 4 | { 5 | "name": "Tom", 6 | "age": 23, 7 | "sex": "male" 8 | }, 9 | { 10 | "name": "Sophia", 11 | "age": 18, 12 | "sex": "female" 13 | }, 14 | { 15 | "name": "Jerry", 16 | "age": 20, 17 | "sex": "male" 18 | }, 19 | { 20 | "name": "Debbie", 21 | "age": 19, 22 | "sex": "female" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/examples/webpack.multiple-entries.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const PATH_DIST = path.join(__dirname, '../../dist'); 3 | const PATH_TARGET = path.join(__dirname, '../templates/basic'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: { 8 | index: path.join(PATH_TARGET, 'index.js'), 9 | utils: path.join(PATH_TARGET, 'utils.js') 10 | }, 11 | output: { 12 | path: PATH_DIST, 13 | filename: '[name].bundle.js' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:prettier/recommended' 5 | ], 6 | plugins: ['prettier'], 7 | env: { 8 | node: true, 9 | es6: true 10 | }, 11 | parserOptions: { 12 | ecmaVersion: 2019, 13 | sourceType: 'module' 14 | }, 15 | rules: { 16 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 17 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 18 | 'prettier/prettier': 'error' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/hello-plugin.js: -------------------------------------------------------------------------------- 1 | class HelloPlugin { 2 | // 在构造函数中获取用户给该插件传入的配置 3 | constructor(options) { 4 | this.options = options; 5 | } 6 | // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象 7 | apply(compiler) { 8 | compiler.hooks.run.tap('MyPlugin', () => console.log('开始编译...')); 9 | compiler.hooks.compile.tap('MyPlugin', async () => { 10 | await new Promise((resolve) => 11 | setTimeout(() => { 12 | console.log('编译中...'); 13 | resolve(); 14 | }, 1000) 15 | ); 16 | }); 17 | } 18 | } 19 | 20 | module.exports = HelloPlugin; 21 | -------------------------------------------------------------------------------- /src/examples/webpack.my.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: path.join(__dirname, '../templates/loaders/test-my.js'), 6 | output: { 7 | path: path.join(__dirname, '../../dist'), 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.my$/, 14 | use: [ 15 | { 16 | loader: path.join(__dirname, '../loaders/my-loader'), 17 | options: { 18 | age: 12 19 | } 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/examples/webpack.async.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: path.join(__dirname, '../templates/basic/async_import.js'), 8 | output: { 9 | path: path.join(__dirname, '../../dist'), 10 | filename: '[name].js' 11 | }, 12 | plugins: [new HtmlWebpackPlugin(), new FriendlyErrorsWebpackPlugin()], 13 | devServer: { 14 | contentBase: path.join(__dirname, '../../dist'), 15 | compress: true, 16 | port: 9000 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Git Commit 提交规范 3 | * ------------------------------------------------ 4 | * upd:更新某功能(不是 feat, 不是 fix) 5 | * feat:新功能(feature) 6 | * fix:修补bug 7 | * docs:文档(documentation) 8 | * style:格式(不影响代码运行的变动) 9 | * refactor:重构(即不是新增功能,也不是修改bug的代码变动) 10 | * test:增加测试 11 | * chore:构建过程或辅助工具的变动 12 | */ 13 | 14 | module.exports = { 15 | extends: ['@commitlint/config-conventional'], 16 | rules: { 17 | 'type-enum': [2, 'always', ['upd', 'feat', 'fix', 'refactor', 'docs', 'chore', 'style', 'revert']], 18 | 'type-case': [0], 19 | 'type-empty': [0], 20 | 'scope-empty': [0], 21 | 'scope-case': [0], 22 | 'subject-full-stop': [0, 'never'], 23 | 'subject-case': [0, 'never'], 24 | 'header-max-length': [0, 'always', 72] 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 2012 2013 Forward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/examples/webpack.css.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 5 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); 6 | 7 | module.exports = { 8 | mode: 'development', 9 | entry: path.join(__dirname, '../templates/loaders/test-css.js'), 10 | output: { 11 | path: path.join(__dirname, '../../dist'), 12 | filename: 'bundle-css.js' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.css$/, 18 | use: [ 19 | // 'style-loader', 20 | MiniCssExtractPlugin.loader, 21 | 'css-loader' 22 | ] 23 | } 24 | ] 25 | }, 26 | plugins: [ 27 | new HtmlWebpackPlugin(), 28 | new MiniCssExtractPlugin({ 29 | filename: '[name].css', 30 | chunkFilename: '[id].css' 31 | }), 32 | new OptimizeCssAssetsPlugin(), 33 | new FriendlyErrorsWebpackPlugin() 34 | ], 35 | devServer: { 36 | contentBase: path.join(__dirname, '../../dist'), 37 | compress: true, 38 | port: 9000 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | // max 100 characters per line 4 | printWidth: 100, 5 | // use 2 spaces for indentation 6 | tabWidth: 2, 7 | // use spaces instead of indentations 8 | useTabs: false, 9 | // semicolon at the end of the line 10 | semi: true, 11 | // use single quotes 12 | singleQuote: true, 13 | // object's key is quoted only when necessary 14 | quoteProps: 'as-needed', 15 | // use double quotes instead of single quotes in jsx 16 | jsxSingleQuote: false, 17 | // no comma at the end 18 | trailingComma: 'none', 19 | // spaces are required at the beginning and end of the braces 20 | bracketSpacing: true, 21 | // end tag of jsx need to wrap 22 | jsxBracketSameLine: false, 23 | // brackets are required for arrow function parameter, even when there is only one parameter 24 | arrowParens: 'always', 25 | // format the entire contents of the file 26 | rangeStart: 0, 27 | rangeEnd: Infinity, 28 | // no need to write the beginning @prettier of the file 29 | requirePragma: false, 30 | // No need to automatically insert @prettier at the beginning of the file 31 | insertPragma: false, 32 | // use default break criteria 33 | proseWrap: 'preserve', 34 | // decide whether to break the html according to the display style 35 | htmlWhitespaceSensitivity: 'css', 36 | // lf for newline 37 | endOfLine: 'lf' 38 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learning-webpack", 3 | "version": "0.1.0", 4 | "author": "vincent0700 (https://vincentstudio.info)", 5 | "email": "wang.yuanqiu007@gmail.com", 6 | "description": "A project to learn webpack.", 7 | "license": "MIT", 8 | "keywords": [], 9 | "scripts": { 10 | "dev:css": "webpack-dev-server --config=./src/examples/webpack.css.js", 11 | "dev:async": "webpack-dev-server --config=./src/examples/webpack.async.js", 12 | "build:basic": "rimraf dist && webpack --config=./src/examples/webpack.basic.js", 13 | "build:multiple-entries": "rimraf dist && webpack --config=./src/examples/webpack.multiple-entries.js", 14 | "build:css": "rimraf dist && webpack --config=./src/examples/webpack.css.js", 15 | "build:async": "rimraf dist && webpack --config=./src/examples/webpack.async.js", 16 | "build:my": "rimraf dist && webpack --config=./src/examples/webpack.my.js", 17 | "build:hello-plugin": "rimraf dist && webpack --config=./src/examples/webpack.hello-plugin.js", 18 | "lint": "eslint --fix . && prettier --write ./**/*.{md,html,json,css,scss,less}" 19 | }, 20 | "devDependencies": { 21 | "@commitlint/cli": "^11.0.0", 22 | "@commitlint/config-conventional": "^11.0.0", 23 | "css-loader": "^3.5.3", 24 | "eslint": "^6.8.0", 25 | "eslint-config-prettier": "^6.9.0", 26 | "eslint-plugin-prettier": "^3.1.2", 27 | "friendly-errors-webpack-plugin": "^1.7.0", 28 | "html-webpack-plugin": "^4.3.0", 29 | "husky": "^4.2.1", 30 | "lint-staged": "^10.0.3", 31 | "mini-css-extract-plugin": "^0.9.0", 32 | "optimize-css-assets-webpack-plugin": "^5.0.3", 33 | "prettier": "^1.19.1", 34 | "rimraf": "^3.0.2", 35 | "style-loader": "^1.2.1", 36 | "webpack": "^4.43.0", 37 | "webpack-cli": "^3.3.11", 38 | "webpack-dev-server": "^3.11.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/Webpack模块化原理.md: -------------------------------------------------------------------------------- 1 | # Webpack 模块化原理 2 | 3 | > 本文旨在通过分析 Webpack 打包后代码的方式来探索其模块化原理。 4 | 5 | ## 示例源码 6 | 7 | ```bash 8 | $ git clone https://github.com/Vincent0700/learning-webpack.git 9 | $ cd learning-webpack 10 | $ yarn install 11 | $ yarn build:basic 12 | ``` 13 | 14 | ### 待打包文件 15 | 16 | ```javascript 17 | // src/templates/basic/utils.js 18 | export const add = (x, y) => x + y; 19 | export const num = 10; 20 | export const obj = { a: { b: 1 } }; 21 | 22 | export default { 23 | add, 24 | num, 25 | obj 26 | }; 27 | ``` 28 | 29 | ```javascript 30 | // src/templates/basic/index.js 31 | import utils from './utils'; 32 | 33 | const result = utils.add(1, 2); 34 | console.log(result); 35 | ``` 36 | 37 | ### Webpack 配置 38 | 39 | ```javascript 40 | // src/examples/webpack.basic.js 41 | const path = require('path'); 42 | 43 | module.exports = { 44 | mode: 'development', 45 | entry: path.join(__dirname, '../templates/index.js'), 46 | output: { 47 | path: path.join(__dirname, '../../dist'), 48 | filename: 'bundle.js' 49 | } 50 | }; 51 | ``` 52 | 53 | ## 打包结果 54 | 55 | 我格式化并删减了一写注释,得到的 `bundle.js` 文件内容如下: 56 | 57 | ```javascript 58 | (function(modules) { 59 | // webpackBootstrap 60 | // The module cache 61 | var installedModules = {}; 62 | 63 | // The require function 64 | function __webpack_require__(moduleId) { 65 | // Check if module is in cache 66 | if (installedModules[moduleId]) { 67 | return installedModules[moduleId].exports; 68 | } 69 | // Create a new module (and put it into the cache) 70 | var module = (installedModules[moduleId] = { 71 | i: moduleId, 72 | l: false, 73 | exports: {} 74 | }); 75 | 76 | // Execute the module function 77 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 78 | 79 | // Flag the module as loaded 80 | module.l = true; 81 | 82 | // Return the exports of the module 83 | return module.exports; 84 | } 85 | 86 | // expose the modules object (__webpack_modules__) 87 | __webpack_require__.m = modules; 88 | 89 | // expose the module cache 90 | __webpack_require__.c = installedModules; 91 | 92 | // define getter function for harmony exports 93 | __webpack_require__.d = function(exports, name, getter) { 94 | if (!__webpack_require__.o(exports, name)) { 95 | Object.defineProperty(exports, name, { enumerable: true, get: getter }); 96 | } 97 | }; 98 | 99 | // define __esModule on exports 100 | __webpack_require__.r = function(exports) { 101 | if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { 102 | Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 103 | } 104 | Object.defineProperty(exports, '__esModule', { value: true }); 105 | }; 106 | 107 | // create a fake namespace object 108 | // mode & 1: value is a module id, require it 109 | // mode & 2: merge all properties of value into the ns 110 | // mode & 4: return value when already ns object 111 | // mode & 8|1: behave like require 112 | __webpack_require__.t = function(value, mode) { 113 | if (mode & 1) value = __webpack_require__(value); 114 | if (mode & 8) return value; 115 | if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value; 116 | var ns = Object.create(null); 117 | __webpack_require__.r(ns); 118 | Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 119 | if (mode & 2 && typeof value != 'string') 120 | for (var key in value) 121 | __webpack_require__.d( 122 | ns, 123 | key, 124 | function(key) { 125 | return value[key]; 126 | }.bind(null, key) 127 | ); 128 | return ns; 129 | }; 130 | 131 | // getDefaultExport function for compatibility with non-harmony modules 132 | __webpack_require__.n = function(module) { 133 | var getter = 134 | module && module.__esModule 135 | ? function getDefault() { 136 | return module['default']; 137 | } 138 | : function getModuleExports() { 139 | return module; 140 | }; 141 | __webpack_require__.d(getter, 'a', getter); 142 | return getter; 143 | }; 144 | 145 | // Object.prototype.hasOwnProperty.call 146 | __webpack_require__.o = function(object, property) { 147 | return Object.prototype.hasOwnProperty.call(object, property); 148 | }; 149 | 150 | // __webpack_public_path__ 151 | __webpack_require__.p = ''; 152 | 153 | // Load entry module and return exports 154 | return __webpack_require__((__webpack_require__.s = './src/templates/index.js')); 155 | })({ 156 | './src/templates/index.js': 157 | /*! ./src/templates/index.js */ 158 | function(module, __webpack_exports__, __webpack_require__) { 159 | 'use strict'; 160 | eval(` 161 | __webpack_require__.r(__webpack_exports__); 162 | var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/templates/utils.js"); 163 | const result = _utils__WEBPACK_IMPORTED_MODULE_0__["default"].add(1, 2); 164 | console.log(result); 165 | `); 166 | }, 167 | 168 | './src/templates/utils.js': 169 | /*! ./src/templates/utils.js */ 170 | function(module, __webpack_exports__, __webpack_require__) { 171 | 'use strict'; 172 | eval(` 173 | __webpack_require__.r(__webpack_exports__); 174 | __webpack_require__.d(__webpack_exports__, "add", function() { return add; }); 175 | __webpack_require__.d(__webpack_exports__, "num", function() { return num; }); 176 | __webpack_require__.d(__webpack_exports__, "obj", function() { return obj; }); 177 | const add = (x, y) => x + y; 178 | const num = 10; 179 | const obj = { a: { b: 1 } }; 180 | __webpack_exports__["default"] = ({ add, num, obj }); 181 | `); 182 | } 183 | }); 184 | ``` 185 | 186 | ## 源码分析 187 | 188 | ### `IFFE` 189 | 190 | 打包后的整体就是一个立即执行函数,精简结构如下: 191 | 192 | ```javascript 193 | (function(modules) { 194 | var installedModules = {}; 195 | function __webpack_require__(moduleId) { 196 | // add some magic ... 197 | return module.exports; 198 | } 199 | return __webpack_require__('index.js'); 200 | })({ 201 | 'index.js': function(module, __webpack_exports__, __webpack_require__) { 202 | eval('...'); 203 | }, 204 | 'utils.js': function(module, __webpack_exports__, __webpack_require__) { 205 | eval('...'); 206 | } 207 | }); 208 | ``` 209 | 210 | ### 核心函数 `__webpack_require__` 211 | 212 | ```javascript 213 | function __webpack_require__(moduleId) { 214 | // 如果缓存了已装载的模块,则不重复执行,直接返回导出的引用 215 | if (installedModules[moduleId]) { 216 | return installedModules[moduleId].exports; 217 | } 218 | // 缓存没命中则构建模块 219 | var module = (installedModules[moduleId] = { 220 | i: moduleId, 221 | l: false, 222 | exports: {} 223 | }); 224 | // 执行模块 225 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 226 | // 模块装载标志 227 | module.l = true; 228 | // 返回导出的引用 229 | return module.exports; 230 | } 231 | ``` 232 | 233 | 从上述代码可以看出: 234 | 235 | 1. 模块代码只执行一次,缓存在 `modules[moduleId]` 236 | 2. 模块执行后导出对象会挂在 `module.exports` 并返回 237 | 238 | 我们来看看 `module` 对象 239 | 240 | ```javascript 241 | // index.js 242 | { 243 | i: "./src/templates/index.js", 244 | l: true 245 | exports: { 246 | Symbol.toStringTag: "Module", 247 | __esModule: true 248 | } 249 | } 250 | ``` 251 | 252 | ```javascript 253 | // utils.js 254 | { 255 | i: "./src/templates/utils.js" 256 | l: true 257 | exports: { 258 | add: (x, y) => x + y, 259 | divide: (x, y) => x / y, 260 | minus: (x, y) => x - y, 261 | multiply: (x, y) => x * y, 262 | default: { 263 | add: (x, y) => x + y, 264 | divide: (x, y) => x / y, 265 | minus: (x, y) => x - y, 266 | multiply: (x, y) => x * y 267 | }, 268 | Symbol.toStringTag: "Module", 269 | __esModule: true 270 | } 271 | } 272 | ``` 273 | 274 | 从上述代码可以看出: 275 | 276 | 1. `module.i` 即 `moduleId`,为模块的相对路径 277 | 2. `module.l` 将会在模块代码执行后置为 `true` 278 | 3. `export { a }` 将会转化为 `module.exports.a` 279 | 4. `export default b` 将会转化为 `module.exports.b` 280 | 5. `Symbol.toStringTag` 是一个内置 `symbol`,使得我们可以通过 `Object.prototype.toString(module)` 得到 `[object Module]` 以推断类型 281 | 6. `__esModule` 标志了这是一个符合 `ES` 标准的模块 282 | 283 | ### 参数部分 284 | 285 | 最后研究一下,`IFFE` 的参数部分,即模块代码的编译结果: 286 | 287 | ```javascript 288 | { 289 | './src/templates/index.js': 290 | function(module, __webpack_exports__, __webpack_require__) { 291 | 'use strict'; 292 | eval(` 293 | __webpack_require__.r(__webpack_exports__); 294 | var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/templates/utils.js"); 295 | const result = _utils__WEBPACK_IMPORTED_MODULE_0__["default"].add(1, 2); 296 | console.log(result); 297 | `); 298 | }, 299 | './src/templates/utils.js': 300 | function(module, __webpack_exports__, __webpack_require__) { 301 | 'use strict'; 302 | eval(` 303 | __webpack_require__.r(__webpack_exports__); 304 | __webpack_require__.d(__webpack_exports__, "add", function() { return add; }); 305 | __webpack_require__.d(__webpack_exports__, "num", function() { return num; }); 306 | __webpack_require__.d(__webpack_exports__, "obj", function() { return obj; }); 307 | const add = (x, y) => x + y; 308 | const num = 10; 309 | const obj = { a: { b: 1 } }; 310 | __webpack_exports__["default"] = ({ add, num, obj }); 311 | `); 312 | } 313 | } 314 | ``` 315 | 316 | 代码分析: 317 | 318 | 1. `__webpack_exports__` 即执行前初始化的 `module.export = {}`,在代码执行时传入,执行后赋以用 `export` 和 `export default` 导出的值或对象 319 | 2. `__webpack_require__.r` 函数定义了 `module.exports.__esModule = true` 320 | 3. `__webpack_require__.d` 函数即在 `module.exports` 上定义导出的变量 321 | 4. `export default obj` 将会转化为 `module.exports.default = obj` 322 | 5. `import utils from './utils'` 将会通过 `__webpack_require__` 导入,根据前面的分析可以得出,模块代码执行的顺序应该是从入口点开始,`import` 的顺序,如果有嵌套引入,则会根据执行嵌套的顺序依次执行后标记引入。 323 | 6. 和 `commonjs` 不同,`import` 导入的变量是值的引用 324 | -------------------------------------------------------------------------------- /docs/Webpack动态导入原理.md: -------------------------------------------------------------------------------- 1 | # Webpack 动态导入原理 2 | 3 | > 本文主要记录了 Webpack import('module').then(...) 动态导入语法的原理,如果对 Webpack 模块化原理不是很了解,可以参考我之前的文章 [Webpack 模块化原理](./Webpack模块化原理.md) 4 | 5 | ## 示例源码 6 | 7 | ```bash 8 | $ git clone https://github.com/Vincent0700/learning-webpack.git 9 | $ cd learning-webpack 10 | $ yarn install 11 | # 开发 12 | $ yarn dev:async 13 | # 编译 14 | $ yarn build:async 15 | ``` 16 | 17 | ### 待打包文件 18 | 19 | ```javascript 20 | // src/templates/basic/utils.js 21 | export const add = (x, y) => x + y; 22 | export const num = 10; 23 | export const obj = { a: { b: 1 } }; 24 | 25 | export default { 26 | add, 27 | num, 28 | obj 29 | }; 30 | ``` 31 | 32 | ```javascript 33 | // src/templates/basic/hello.js 34 | export default function(name) { 35 | console.log(`hello ${name}`); 36 | } 37 | ``` 38 | 39 | ```javascript 40 | // src/templates/basic/async_import.js 41 | setTimeout(async () => { 42 | const utils = await import(/* webpackChunkName: "utils" */ './utils'); 43 | const hello = await import(/* webpackChunkName: "hello" */ './hello'); 44 | console.log(utils); 45 | console.log(hello); 46 | }, 3000); 47 | ``` 48 | 49 | 入口文件 `async_import.js` 会在三秒后引入 `utils.js`,从语法可以看出 `import(...)` 的结果是一个 `Promise` 猜测应该是 `Webpack` 的 `module` 对象 50 | 51 | ### Webpack 配置 52 | 53 | ```javascript 54 | // src/examples/webpack.async.js 55 | const path = require('path'); 56 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 57 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); 58 | 59 | module.exports = { 60 | mode: 'development', 61 | entry: path.join(__dirname, '../templates/basic/async_import.js'), 62 | output: { 63 | path: path.join(__dirname, '../../dist'), 64 | filename: '[name].js' 65 | }, 66 | plugins: [new HtmlWebpackPlugin(), new FriendlyErrorsWebpackPlugin()], 67 | devServer: { 68 | contentBase: path.join(__dirname, '../../dist'), 69 | compress: true, 70 | port: 9000 71 | } 72 | }; 73 | ``` 74 | 75 | ## 打包结果 76 | 77 | 我格式化并删减了一写注释,得到的 `utils.js` 内容如下: 78 | 79 | ```javascript 80 | (window['webpackJsonp'] = window['webpackJsonp'] || []).push([ 81 | ['utils'], 82 | { 83 | './src/templates/basic/utils.js': function(module, __webpack_exports__, __webpack_require__) { 84 | eval(` 85 | __webpack_require__.r(__webpack_exports__); 86 | __webpack_require__.d(__webpack_exports__, "add", function() { return add; });__webpack_require__.d(__webpack_exports__, "num", function() { return num; });__webpack_require__.d(__webpack_exports__, "obj", function() { return obj; }); 87 | const add = (x, y) => x + y; 88 | const num = 10; 89 | const obj = { a: { b: 1 } }; 90 | __webpack_exports__["default"] = ({ add, num, obj }); 91 | `); 92 | } 93 | } 94 | ]); 95 | ``` 96 | 97 | 得到的 `main.js` 文件内容如下: 98 | 99 | ```javascript 100 | (function(modules) { 101 | // webpackBootstrap 102 | // install a JSONP callback for chunk loading 103 | function webpackJsonpCallback(data) { 104 | var chunkIds = data[0]; 105 | var moreModules = data[1]; 106 | 107 | // add "moreModules" to the modules object, 108 | // then flag all "chunkIds" as loaded and fire callback 109 | var moduleId, 110 | chunkId, 111 | i = 0, 112 | resolves = []; 113 | for (; i < chunkIds.length; i++) { 114 | chunkId = chunkIds[i]; 115 | if ( 116 | Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && 117 | installedChunks[chunkId] 118 | ) { 119 | resolves.push(installedChunks[chunkId][0]); 120 | } 121 | installedChunks[chunkId] = 0; 122 | } 123 | for (moduleId in moreModules) { 124 | if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { 125 | modules[moduleId] = moreModules[moduleId]; 126 | } 127 | } 128 | if (parentJsonpFunction) parentJsonpFunction(data); 129 | 130 | while (resolves.length) { 131 | resolves.shift()(); 132 | } 133 | } 134 | 135 | // The module cache 136 | var installedModules = {}; 137 | 138 | // object to store loaded and loading chunks 139 | // undefined = chunk not loaded, null = chunk preloaded/prefetched 140 | // Promise = chunk loading, 0 = chunk loaded 141 | var installedChunks = { 142 | main: 0 143 | }; 144 | 145 | // script path function 146 | function jsonpScriptSrc(chunkId) { 147 | return __webpack_require__.p + '' + ({ utils: 'utils' }[chunkId] || chunkId) + '.js'; 148 | } 149 | 150 | // The require function 151 | function __webpack_require__(moduleId) { 152 | // Check if module is in cache 153 | if (installedModules[moduleId]) { 154 | return installedModules[moduleId].exports; 155 | } 156 | // Create a new module (and put it into the cache) 157 | var module = (installedModules[moduleId] = { 158 | i: moduleId, 159 | l: false, 160 | exports: {} 161 | }); 162 | 163 | // Execute the module function 164 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 165 | 166 | // Flag the module as loaded 167 | module.l = true; 168 | 169 | // Return the exports of the module 170 | return module.exports; 171 | } 172 | 173 | // This file contains only the entry chunk. 174 | // The chunk loading function for additional chunks 175 | __webpack_require__.e = function requireEnsure(chunkId) { 176 | var promises = []; 177 | 178 | // JSONP chunk loading for javascript 179 | 180 | var installedChunkData = installedChunks[chunkId]; 181 | if (installedChunkData !== 0) { 182 | // 0 means "already installed". 183 | 184 | // a Promise means "currently loading". 185 | if (installedChunkData) { 186 | promises.push(installedChunkData[2]); 187 | } else { 188 | // setup Promise in chunk cache 189 | var promise = new Promise(function(resolve, reject) { 190 | installedChunkData = installedChunks[chunkId] = [resolve, reject]; 191 | }); 192 | promises.push((installedChunkData[2] = promise)); 193 | 194 | // start chunk loading 195 | var script = document.createElement('script'); 196 | var onScriptComplete; 197 | 198 | script.charset = 'utf-8'; 199 | script.timeout = 120; 200 | if (__webpack_require__.nc) { 201 | script.setAttribute('nonce', __webpack_require__.nc); 202 | } 203 | script.src = jsonpScriptSrc(chunkId); 204 | 205 | // create error before stack unwound to get useful stacktrace later 206 | var error = new Error(); 207 | onScriptComplete = function(event) { 208 | // avoid mem leaks in IE. 209 | script.onerror = script.onload = null; 210 | clearTimeout(timeout); 211 | var chunk = installedChunks[chunkId]; 212 | if (chunk !== 0) { 213 | if (chunk) { 214 | var errorType = event && (event.type === 'load' ? 'missing' : event.type); 215 | var realSrc = event && event.target && event.target.src; 216 | error.message = 217 | 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; 218 | error.name = 'ChunkLoadError'; 219 | error.type = errorType; 220 | error.request = realSrc; 221 | chunk[1](error); 222 | } 223 | installedChunks[chunkId] = undefined; 224 | } 225 | }; 226 | var timeout = setTimeout(function() { 227 | onScriptComplete({ type: 'timeout', target: script }); 228 | }, 120000); 229 | script.onerror = script.onload = onScriptComplete; 230 | document.head.appendChild(script); 231 | } 232 | } 233 | return Promise.all(promises); 234 | }; 235 | 236 | // expose the modules object (__webpack_modules__) 237 | __webpack_require__.m = modules; 238 | 239 | // expose the module cache 240 | __webpack_require__.c = installedModules; 241 | 242 | // define getter function for harmony exports 243 | __webpack_require__.d = function(exports, name, getter) { 244 | if (!__webpack_require__.o(exports, name)) { 245 | Object.defineProperty(exports, name, { enumerable: true, get: getter }); 246 | } 247 | }; 248 | 249 | // define __esModule on exports 250 | __webpack_require__.r = function(exports) { 251 | if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { 252 | Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 253 | } 254 | Object.defineProperty(exports, '__esModule', { value: true }); 255 | }; 256 | 257 | // create a fake namespace object 258 | // mode & 1: value is a module id, require it 259 | // mode & 2: merge all properties of value into the ns 260 | // mode & 4: return value when already ns object 261 | // mode & 8|1: behave like require 262 | __webpack_require__.t = function(value, mode) { 263 | if (mode & 1) value = __webpack_require__(value); 264 | if (mode & 8) return value; 265 | if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value; 266 | var ns = Object.create(null); 267 | __webpack_require__.r(ns); 268 | Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 269 | if (mode & 2 && typeof value != 'string') 270 | for (var key in value) 271 | __webpack_require__.d( 272 | ns, 273 | key, 274 | function(key) { 275 | return value[key]; 276 | }.bind(null, key) 277 | ); 278 | return ns; 279 | }; 280 | 281 | // getDefaultExport function for compatibility with non-harmony modules 282 | __webpack_require__.n = function(module) { 283 | var getter = 284 | module && module.__esModule 285 | ? function getDefault() { 286 | return module['default']; 287 | } 288 | : function getModuleExports() { 289 | return module; 290 | }; 291 | __webpack_require__.d(getter, 'a', getter); 292 | return getter; 293 | }; 294 | 295 | // Object.prototype.hasOwnProperty.call 296 | __webpack_require__.o = function(object, property) { 297 | return Object.prototype.hasOwnProperty.call(object, property); 298 | }; 299 | 300 | // __webpack_public_path__ 301 | __webpack_require__.p = ''; 302 | 303 | // on error function for async loading 304 | __webpack_require__.oe = function(err) { 305 | console.error(err); 306 | throw err; 307 | }; 308 | 309 | var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []); 310 | var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); 311 | jsonpArray.push = webpackJsonpCallback; 312 | jsonpArray = jsonpArray.slice(); 313 | for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); 314 | var parentJsonpFunction = oldJsonpFunction; 315 | 316 | // Load entry module and return exports 317 | return __webpack_require__((__webpack_require__.s = './src/templates/basic/async_import.js')); 318 | })({ 319 | './src/templates/basic/async_import.js': function(module, exports, __webpack_require__) { 320 | eval(` 321 | setTimeout(async () => { 322 | const utils = await __webpack_require__.e("utils").then( 323 | __webpack_require__.bind(null, "./src/templates/basic/utils.js") 324 | ); 325 | console.log(utils); 326 | const result = utils.add(1, 2); 327 | console.log(result); 328 | }, 3000) 329 | `); 330 | } 331 | }); 332 | ``` 333 | 334 | ## 源码分析 335 | 336 | 从代码中可以发现, `import('utils')` 被翻译成了 337 | 338 | ``` 339 | __webpack_require__.e('utils') 340 | .then(__webpack_require__.bind(null, './src/templates/basic/utils.js')); 341 | ``` 342 | 343 | 从我之前的文章 [Webpack 模块化原理](./Webpack模块化原理.md) 中可以知道 `__webpack_require__(moduleId)` 会先读取缓存,如果缓存没有命中,就会从 `modules` 加载并执行, 现在被嵌入到 `__webpack_require__.e('utils')` 的 `Promise` 回调中, 所以 `__webpack_require__.e('utils')` 应该会异步加载 `utils.js` 到 `modules` 对象, 然后被 `__webpack_require__` 引入执行。 344 | 345 | 那么 `Webpack` 是如何实现异步加载的呢?我们来看一下 `__webpack_require__.e` 的部分代码: 346 | 347 | ```javascript 348 | var script = document.createElement('script'); 349 | var onScriptComplete; 350 | script.charset = 'utf-8'; 351 | script.timeout = 120; 352 | script.src = jsonpScriptSrc(chunkId); 353 | 354 | onScriptComplete = function(event) { 355 | // ... 356 | }; 357 | 358 | var timeout = setTimeout(function() { 359 | onScriptComplete({ type: 'timeout', target: script }); 360 | }, 120000); 361 | 362 | script.onerror = script.onload = onScriptComplete; 363 | document.head.appendChild(script); 364 | ``` 365 | 366 | 明白了么?`Webpack` 其实是通过 `jsonp` 的方式来实现模块的动态加载的。下面我们来看看 `chunk` 部分: 367 | 368 | ``` 369 | (window['webpackJsonp'] = window['webpackJsonp'] || []).push([ 370 | ['utils'], { 371 | './src/templates/basic/utils.js': 372 | function(module, __webpack_exports__, __webpack_require__) { 373 | ... 374 | } 375 | } 376 | ]); 377 | ``` 378 | 379 | 不难发现,通过 `script` 引入的模块代码最终会挂载 `window.webpackJsonp` 上,我们看一下这个变量的结构: 380 | 381 | ``` 382 | // webpack.webpackJsonp 383 | [ 384 | 0: [ 385 | ["utils"], 386 | {./src/templates/basic/utils.js: ƒ} 387 | ], 388 | 1: [ 389 | ["hello"], 390 | {./src/templates/basic/hello.js: ƒ} 391 | ], 392 | push: f webpackJsonpCallback(data) 393 | ] 394 | ``` 395 | 396 | 我觉得这里 `Webpack` 可能忽视了一个问题,因为这里模块代码是通过全局变量和入口模块进行通信的,就不可避免的会遇变量被污染的情况,我试了下,如果在全局先定义了 `webpackJsonp = 1`,那么后续所有动态引入的模块都无法被加载。 397 | 398 | 最后我转一张掘金上看到的图,展示 `Webpack` 异步加载的流程,[文章链接](https://juejin.im/post/5d26e7d1518825290726f67a) 399 | 400 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be5408cd5fedcb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 401 | -------------------------------------------------------------------------------- /docs/Webpack模块联邦原理.md: -------------------------------------------------------------------------------- 1 | # Webpack5 模块联邦原理 2 | 3 | 这两天把玩了新玩具 Webpack5,模块联邦的新特性让我眼前一亮,下面用我自己实验的例子逐行代码跟踪调试。 4 | 5 | ## 案例 6 | 7 | 这里有 `app1` 和 `app2` 两个完全独立的项目,`app1` 暴露了一个模块 `say` 出去,然后 `app2` 想要去调用它。如果用一般的思维,我们会讲这个 `say` 模块抽成一个公共的包,通过 npm 去共享。但是一旦该模块更新,所有引用这个包的位置也需要 `npm install`。Webpack v5 提供了一种让代码直接在 CDN 中共享的机制,从而不再需要本地安装 npm 包、构建再发布了。我精简后的代码如下: 8 | 9 | ```js 10 | // app1/webpack.config.js 11 | module.exports = { 12 | ... 13 | plugins: [ 14 | new ModuleFederationPlugin({ 15 | name: "app1", 16 | library: { type: "var", name: "app1" }, 17 | filename: "remoteEntry.js", 18 | exposes: { 19 | './say': path.join(__dirname, './say.js') 20 | } 21 | }) 22 | ] 23 | }; 24 | ``` 25 | 26 | ```js 27 | // app2/webpack.config.js 28 | module.exports = { 29 | ... 30 | plugins: [ 31 | new ModuleFederationPlugin({ 32 | name: "app2", 33 | library: { type: "var", name: "app2" }, 34 | remotes: { 35 | app1: "app1", 36 | } 37 | }) 38 | ] 39 | }; 40 | ``` 41 | 42 | ```html 43 | 44 | 45 | ``` 46 | 47 | ```js 48 | // app2/index.js 49 | const remoteSay = import('app1/say'); 50 | remoteSay.then(({ say }) => { 51 | say('app2'); 52 | }); 53 | ``` 54 | 55 | 可以看到,通过引如 `app1` 中定义的远程模块入口文件 `remoteEntry.js` 之后,我们就能够在代码中通过异步模块的方式使用了。 56 | 57 | ## 异步模块原理 58 | 59 | 我们复习下 Webpack v4 中的异步模块的原理: 60 | 61 | 1. `import(chunkId) => __webpack_require__.e(chunkId)` 62 | 将相关的请求回调存入 `installedChunks`。 63 | 64 | ```js 65 | // import(chunkId) => __webpack_require__.e(chunkId) 66 | __webpack_require__.e = function(chunkId) { 67 | return new Promise((resolve, reject) => { 68 | var script = document.createElement('script'); 69 | script.src = jsonpScriptSrc(chunkId); 70 | var onScriptComplete = function(event) { 71 | // ... 72 | }; 73 | var timeout = setTimeout(function() { 74 | onScriptComplete({ type: 'timeout', target: script }); 75 | }, 120000); 76 | script.onerror = script.onload = onScriptComplete; 77 | document.head.appendChild(script); 78 | }); 79 | }; 80 | ``` 81 | 82 | 2. 发起 JSONP 请求 83 | 3. 将下载的模块录入 modules 84 | 4. 执行 chunk 请求回调 85 | 5. 加载 module 86 | 6. 执行用户回调 87 | 88 | ## 模块联邦实现原理 89 | 90 | 首先看 `app2` 打包后的代码,我精简了一下,大致结构如下 91 | 92 | ```js 93 | // 最外层是一个 IIFE 94 | (() => { 95 | var __webpack_modules__ = { 96 | 'webpack/container/reference/app1': 97 | /*!***********************!*\ 98 | !*** external "app1" ***! 99 | \***********************/ 100 | (module) => { 101 | 'use strict'; 102 | module.exports = app1; 103 | } 104 | }; 105 | 106 | // 定义模块缓存 107 | var __webpack_module_cache__ = {}; 108 | 109 | // 定义 __webpack_require__ 110 | function __webpack_require__(moduleId) { 111 | // 尝试从缓存读取模块 112 | if (__webpack_module_cache__[moduleId]) { 113 | return __webpack_module_cache__[moduleId].exports; 114 | } 115 | // 创建模块缓存 116 | var module = (__webpack_module_cache__[moduleId] = { 117 | exports: {} 118 | }); 119 | 120 | // 执行模块回调,从这里可以看出,模块的回调方法存在 __webpack_modules__ 里 121 | __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 122 | 123 | // 返回模块 exports 124 | return module.exports; 125 | } 126 | 127 | // 一些 webpack runtime 方法 ... 128 | 129 | // 底部是本地 app2 的模块代码 130 | (() => { 131 | /*!********************************!*\ 132 | !*** ./examples/app2/index.js ***! 133 | \********************************/ 134 | const remoteSay = __webpack_require__ 135 | .e('webpack_container_remote_app1_say') 136 | .then( 137 | __webpack_require__.t.bind(__webpack_require__, 'webpack/container/remote/app1/say', 7) 138 | ); 139 | remoteSay.then(({ say }) => { 140 | say('app2'); 141 | }); 142 | })(); 143 | })(); 144 | ``` 145 | 146 | 我们可以看到相比于 Webpack v4,打包后代码结构上的变化。首先,在最顶部会暴露依赖的远程模块的入口点,接着 **webpack_require** 的定义没有什么变化,再下面是一堆 runtime 方法。最底部是我们的模块代码。 147 | 148 | 我们原本的 149 | 150 | ```js 151 | const remoteSay = import('app1/say'); 152 | ``` 153 | 154 | 被替换成了 155 | 156 | ```js 157 | const remoteSay = __webpack_require__ 158 | .e('webpack_container_remote_app1_say') 159 | .then(__webpack_require__.t.bind(__webpack_require__, 'webpack/container/remote/app1/say', 7)); 160 | ``` 161 | 162 | 我们切到 `remoteSay` 定义的这一行断点调试,首先是 `__webpack_require__.e` 方法: 163 | 164 | ```js 165 | /* webpack/runtime/ensure chunk */ 166 | (() => { 167 | __webpack_require__.f = {}; 168 | __webpack_require__.e = (chunkId) => { 169 | return Promise.all( 170 | Object.keys(__webpack_require__.f).reduce((promises, key) => { 171 | __webpack_require__.f[key](chunkId, promises); 172 | return promises; 173 | }, []) 174 | ); 175 | }; 176 | })(); 177 | ``` 178 | 179 | 这里,`chunkId` 是 `webpack_container_remote_app1_say`,也就是我们在 `app1` 中暴露的远程模块。**webpack_require**.f 上有两个对象,remotes 和 j,定义如下: 180 | 181 | ```js 182 | // 这里 f.j 方法应该只是把指定的 chunk 标记为已安装 183 | __webpack_require__.f.j = (chunkId, promises) => { 184 | installedChunks[chunkId] = 0; 185 | }; 186 | // 重点在 f.remotes 上 187 | var chunkMapping = { 188 | webpack_container_remote_app1_say: ['webpack/container/remote/app1/say'] 189 | }; 190 | var idToExternalAndNameMapping = { 191 | 'webpack/container/remote/app1/say': ['default', './say', 'webpack/container/reference/app1'] 192 | }; 193 | __webpack_require__.f.remotes = (chunkId, promises) => { 194 | // __webpack_require__.o => hasOwnProperty 195 | if (__webpack_require__.o(chunkMapping, chunkId)) { 196 | chunkMapping[chunkId].forEach((id) => { 197 | var getScope = __webpack_require__.R; 198 | if (!getScope) getScope = []; 199 | var data = idToExternalAndNameMapping[id]; 200 | if (getScope.indexOf(data) >= 0) return; 201 | // getScope = data = ['default', './say', 'webpack/container/reference/app1'] 202 | getScope.push(data); 203 | if (data.p) return promises.push(data.p); 204 | var onError = (error) => { 205 | if (!error) error = new Error('Container missing'); 206 | if (typeof error.message === 'string') 207 | error.message += '\nwhile loading "' + data[1] + '" from ' + data[2]; 208 | __webpack_modules__[id] = () => { 209 | throw error; 210 | }; 211 | data.p = 0; 212 | }; 213 | var handleFunction = (fn, arg1, arg2, d, next, first) => { 214 | /** 215 | * fn: __webpack_require__ 216 | * arg1: 'webpack/container/reference/app1' 217 | * arg2: 0 218 | * d: 0 219 | * next: onExternal 220 | * first: 1 221 | */ 222 | try { 223 | // __webpack_require__('webpack/container/reference/app1', 0) 224 | // 这里会加载模块最顶部导出的从 remoteEntry 暴露出来的 app1 模块 225 | var promise = fn(arg1, arg2); 226 | // 由于返回的结果不是 promise,直接调到 else 227 | if (promise && promise.then) { 228 | var p = promise.then((result) => next(result, d), onError); 229 | if (first) promises.push((data.p = p)); 230 | else return p; 231 | } else { 232 | // 调用 onExternal(app1, 0, 1) 233 | return next(promise, d, first); 234 | } 235 | } catch (error) { 236 | onError(error); 237 | } 238 | }; 239 | var onExternal = (external, _, first) => 240 | external 241 | ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) 242 | : onError(); 243 | var onInitialized = (_, external, first) => 244 | handleFunction(external.get, data[1], getScope, 0, onFactory, first); 245 | var onFactory = (factory) => { 246 | data.p = 1; 247 | __webpack_modules__[id] = (module) => { 248 | module.exports = factory(); 249 | }; 250 | }; 251 | handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1); 252 | }); 253 | } 254 | }; 255 | ``` 256 | 257 | 第一次 `handleFunction` 会用 **webpack_require** 读取文件最顶部定义的 `app1` 的 chunk,这个 chunk 最终会导出 `app1` 的入口文件模块 `remoteEntry.js`。 258 | 259 | 由于 `remoteEntry` 是最先加载的,所以直接返回 `module` 本身而不是 `promise`,所以直接跳到 `else` 执行 `onExternal(app1, 0, 1)`。 260 | 261 | 第二次执行 `handleFunction`: 262 | 263 | ```js 264 | var handleFunction = (fn, arg1, arg2, d, next, first) => { 265 | // __webpack_require__.I('default', 0) 266 | var promise = fn(arg1, arg2); 267 | ... 268 | }; 269 | ``` 270 | 271 | 这里首先调用 **webpack_require**.I('default'),我们看下 I 方法: 272 | 273 | ```js 274 | /* webpack/runtime/sharing */ 275 | (() => { 276 | __webpack_require__.S = {}; 277 | var initPromises = {}; 278 | var initTokens = {}; 279 | __webpack_require__.I = (name, initScope) => { 280 | // 初始化 initScope 对象 281 | if (!initScope) initScope = []; 282 | // 解决 init 方法循环调用的问题,如果初始化过 initScope,则直接从缓存中读取 283 | var initToken = initTokens[name]; 284 | if (!initToken) initToken = initTokens[name] = {}; 285 | if (initScope.indexOf(initToken) >= 0) return; 286 | initScope.push(initToken); 287 | // 处理异步 init 方法 288 | if (initPromises[name]) return initPromises[name]; 289 | // 收集 init 方法的调用依赖,挂在 __webpack_require__.S 上,如果没有则新建空对象 290 | if (!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {}; 291 | // share scope,即为,init 方法的执行环境 292 | var scope = __webpack_require__.S[name]; 293 | var warn = (msg) => typeof console !== 'undefined' && console.warn && console.warn(msg); 294 | // 这个 uniqueName 最终作为全局变量 window[webpackChunk + uniqueName] 作为远程模块回调的缓存 295 | var uniqueName = 'webpack5-demo'; 296 | var register = (name, version, factory) => { 297 | var versions = (scope[name] = scope[name] || {}); 298 | var activeVersion = versions[version]; 299 | if (!activeVersion || (!activeVersion.loaded && uniqueName > activeVersion.from)) 300 | versions[version] = { get: factory, from: uniqueName }; 301 | }; 302 | // 初始化外部模块 303 | var initExternal = (id) => { 304 | var handleError = (err) => warn('Initialization of sharing external failed: ' + err); 305 | try { 306 | // 拿到 app1 307 | var module = __webpack_require__(id); 308 | if (!module) return; 309 | // 重要!调用 app1.init 方法初始化,之前所有收集依赖的步骤都是为了给这里创造执行环境 310 | var initFn = (module) => 311 | module && module.init && module.init(__webpack_require__.S[name], initScope); 312 | if (module.then) return promises.push(module.then(initFn, handleError)); 313 | var initResult = initFn(module); 314 | if (initResult && initResult.then) return promises.push(initResult.catch(handleError)); 315 | } catch (err) { 316 | handleError(err); 317 | } 318 | }; 319 | var promises = []; 320 | switch (name) { 321 | case 'default': 322 | { 323 | initExternal('webpack/container/reference/app1'); 324 | } 325 | break; 326 | } 327 | if (!promises.length) return (initPromises[name] = 1); 328 | return (initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1))); 329 | }; 330 | })(); 331 | ``` 332 | 333 | 执行完毕后回来调用第三次 `handleFunction`: 334 | 335 | ```js 336 | var handleFunction = (fn, arg1, arg2, d, next, first) => { 337 | // app1.get('./say', ['default', './say', 'webpack/container/reference/app1']) 338 | var promise = fn(arg1, arg2); 339 | ... 340 | } 341 | ``` 342 | 343 | 跳到 `remoteEntry` 的 `app1.get` 方法: 344 | 345 | ```js 346 | var moduleMap = { 347 | './say': () => { 348 | return __webpack_require__ 349 | .e('examples_app1_say_js') 350 | .then(() => () => __webpack_require__('./examples/app1/say.js')); 351 | } 352 | }; 353 | var get = (module, getScope) => { 354 | __webpack_require__.R = getScope; 355 | getScope = __webpack_require__.o(moduleMap, module) 356 | ? moduleMap[module]() 357 | : Promise.resolve().then(() => { 358 | throw new Error('Module "' + module + '" does not exist in container.'); 359 | }); 360 | __webpack_require__.R = undefined; 361 | return getScope; 362 | }; 363 | ``` 364 | 365 | 这里在 `moduleMap` 定义了 `./say` 方法所在的异步模块,然后通过 **webpack_require**.e 下载异步模块,加载完之后再调用 **webpack_require** 执行模块回调。看来下载远程模块的代码在 `e` 方法里了: 366 | 367 | ```js 368 | /* webpack/runtime/ensure chunk */ 369 | (() => { 370 | __webpack_require__.f = {}; 371 | __webpack_require__.e = (chunkId) => { 372 | return Promise.all( 373 | Object.keys(__webpack_require__.f).reduce((promises, key) => { 374 | __webpack_require__.f[key](chunkId, promises); 375 | return promises; 376 | }, []) 377 | ); 378 | }; 379 | })(); 380 | ``` 381 | 382 | 在 **webpack_require**.f 中只有一个 `j` 方法,跳转到 **webpack_require**.f.j: 383 | 384 | ```js 385 | __webpack_require__.f.j = (chunkId, promises) => { 386 | var installedChunkData = __webpack_require__.o(installedChunks, chunkId) 387 | ? installedChunks[chunkId] 388 | : undefined; 389 | // installedChunkData 如果等于 0 表明已加载 390 | if (installedChunkData !== 0) { 391 | if (installedChunkData) { 392 | promises.push(installedChunkData[2]); 393 | } else { 394 | if (true) { 395 | // 不太清楚这里的判断啥意思 396 | // 初始化 Promise 397 | var promise = new Promise((resolve, reject) => { 398 | installedChunkData = installedChunks[chunkId] = [resolve, reject]; 399 | }); 400 | promises.push((installedChunkData[2] = promise)); 401 | // 获取 chunk 地址 402 | var url = __webpack_require__.p + __webpack_require__.u(chunkId); 403 | var error = new Error(); 404 | var loadingEnded = (event) => { 405 | if (__webpack_require__.o(installedChunks, chunkId)) { 406 | installedChunkData = installedChunks[chunkId]; 407 | if (installedChunkData !== 0) installedChunks[chunkId] = undefined; 408 | if (installedChunkData) { 409 | var errorType = event && (event.type === 'load' ? 'missing' : event.type); 410 | var realSrc = event && event.target && event.target.src; 411 | error.message = 412 | 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; 413 | error.name = 'ChunkLoadError'; 414 | error.type = errorType; 415 | error.request = realSrc; 416 | installedChunkData[1](error); 417 | } 418 | } 419 | }; 420 | // 下载 chunk 脚本 421 | __webpack_require__.l(url, loadingEnded, 'chunk-' + chunkId); 422 | } else installedChunks[chunkId] = 0; 423 | } 424 | } 425 | }; 426 | ``` 427 | 428 | 通过 **webpack_require**.l(url, errorHandler, chunkName) 下载脚本: 429 | 430 | ```js 431 | /* webpack/runtime/load script */ 432 | (() => { 433 | var inProgress = {}; 434 | var dataWebpackPrefix = 'webpack5-demo:'; 435 | // loadScript function to load a script via script tag 436 | __webpack_require__.l = (url, done, key) => { 437 | if (inProgress[url]) { 438 | inProgress[url].push(done); 439 | return; 440 | } 441 | var script, needAttach; 442 | if (key !== undefined) { 443 | var scripts = document.getElementsByTagName('script'); 444 | for (var i = 0; i < scripts.length; i++) { 445 | var s = scripts[i]; 446 | if ( 447 | s.getAttribute('src') == url || 448 | s.getAttribute('data-webpack') == dataWebpackPrefix + key 449 | ) { 450 | script = s; 451 | break; 452 | } 453 | } 454 | } 455 | if (!script) { 456 | needAttach = true; 457 | // 创建 script 标签 458 | script = document.createElement('script'); 459 | 460 | script.charset = 'utf-8'; 461 | script.timeout = 120; 462 | if (__webpack_require__.nc) { 463 | script.setAttribute('nonce', __webpack_require__.nc); 464 | } 465 | script.setAttribute('data-webpack', dataWebpackPrefix + key); 466 | // 设置 src = 'http://127.0.0.1:2001/examples_app1_say_js.bundle.js' 467 | script.src = url; 468 | // 到这远程脚本 examples_app1_say_js.bundle.js 应该就开始下载了 469 | } 470 | inProgress[url] = [done]; 471 | var onScriptComplete = (prev, event) => { 472 | // avoid mem leaks in IE. 473 | script.onerror = script.onload = null; 474 | clearTimeout(timeout); 475 | var doneFns = inProgress[url]; 476 | delete inProgress[url]; 477 | script.parentNode && script.parentNode.removeChild(script); 478 | doneFns && doneFns.forEach((fn) => fn(event)); 479 | if (prev) return prev(event); 480 | }; 481 | var timeout = setTimeout( 482 | onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 483 | 120000 484 | ); 485 | script.onerror = onScriptComplete.bind(null, script.onerror); 486 | script.onload = onScriptComplete.bind(null, script.onload); 487 | needAttach && document.head.appendChild(script); 488 | }; 489 | })(); 490 | ``` 491 | 492 | 到此,远程模块已加载完成,后面的事情就与 Webpack v4 一样了。 493 | 494 | ## 小结 495 | 496 | 下面总结下远程模块的加载步骤: 497 | 498 | 1. 下载并执行 `remoteEntry.js`,挂载入口点对象到 `window.app1`,他有两个函数属性,`init` 和 `get`。`init` 方法用于初始化作用域对象 initScope,`get` 方法用于下载 `moduleMap` 中导出的远程模块。 499 | 2. 加载 `app1` 到本地模块 500 | 3. 创建 `app1.init` 的执行环境,收集依赖到共享作用域对象 `shareScope` 501 | 4. 执行 `app1.init`,初始化 `initScope` 502 | 5. 用户 `import` 远程模块时调用 `app1.get(moduleName)` 通过 `Jsonp` 懒加载远程模块,然后缓存在全局对象 window['webpackChunk' + appName] 503 | 6. 通过 **webpack_require** 读取缓存中的模块,执行用户回调 504 | -------------------------------------------------------------------------------- /docs/WebpackLoader原理学习之css-loader.md: -------------------------------------------------------------------------------- 1 | # Webpack Loader 原理学习之 css-loader 2 | 3 | 本文通过 `css-loader` 打包后代码分析了其工作原理,以及相关使用细节。如果对 `Webpack` 模块化原理不熟悉的童鞋可以参考我上一篇文章 [Webpack 模块化原理](./Webpack模块化原理.md)。 4 | 5 | ## 示例源码 6 | 7 | 我们依旧从打包后的源码开始看起。 8 | 9 | ```bash 10 | $ git clone https://github.com/Vincent0700/learning-webpack.git 11 | $ cd learning-webpack 12 | $ yarn install 13 | # 运行 14 | $ yarn dev:css 15 | # 打包 16 | $ yarn build:css 17 | ``` 18 | 19 | ### 待打包文件 20 | 21 | ```css 22 | /* src/templates/loaders/test.css */ 23 | html, 24 | body { 25 | background: #ccc; 26 | height: 100vh; 27 | } 28 | 29 | h1 { 30 | font-size: 100px; 31 | } 32 | ``` 33 | 34 | ```javascript 35 | // src/templates/loaders/test-css.js 36 | import style from './test.css'; 37 | console.log(style); 38 | ``` 39 | 40 | ### Webpack 配置 41 | 42 | ```javascript 43 | // src/examples/webpack.css.js 44 | const path = require('path'); 45 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 46 | 47 | module.exports = { 48 | mode: 'development', 49 | entry: path.join(__dirname, '../templates/loaders/test-css.js'), 50 | output: { 51 | path: path.join(__dirname, '../../dist'), 52 | filename: 'bundle-css.js' 53 | }, 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.css$/, 58 | use: ['css-loader'] 59 | } 60 | ] 61 | }, 62 | plugins: [new HtmlWebpackPlugin()], 63 | devServer: { 64 | contentBase: path.join(__dirname, '../../dist'), 65 | compress: true, 66 | port: 9000 67 | } 68 | }; 69 | ``` 70 | 71 | ## 打包结果 72 | 73 | 打包后得到了 `bundle-css.js`,其中有 3 个 `modules`: 74 | 75 | 1. ./node_modules/css-loader/dist/runtime/api.js 76 | 2. ./src/templates/loaders/test-css.js 77 | 3. './src/templates/loaders/test.css' 78 | 79 | 我们从入口 test-css.js 看起 80 | 81 | ```javascript 82 | // ./src/templates/loaders/test-css.js' 83 | function(module, __webpack_exports__, __webpack_require__) { 84 | __webpack_require__.r(__webpack_exports__); 85 | var _test_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( 86 | './src/templates/loaders/test.css' 87 | ); 88 | var _test_css__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n( 89 | _test_css__WEBPACK_IMPORTED_MODULE_0__ 90 | ); 91 | console.log(_test_css__WEBPACK_IMPORTED_MODULE_0___default.a); 92 | }, 93 | ``` 94 | 95 | 这段代码首先调用 `__webpack_require__` 读取 `css` 模块,然后是一段兼容性代码,`__webpack_require__.n`,它的作用是判断是否是 `es6` 模块,如果是导出 `module.default`,否则直接导出 `module`,代码如下: 96 | 97 | ```javascript 98 | __webpack_require__.n = function(module) { 99 | var getter = 100 | module && module.__esModule 101 | ? function getDefault() { 102 | return module['default']; 103 | } 104 | : function getModuleExports() { 105 | return module; 106 | }; 107 | __webpack_require__.d(getter, 'a', getter); 108 | return getter; 109 | }; 110 | ``` 111 | 112 | 接着,我们看下 `css` 模块的代码: 113 | 114 | ```javascript 115 | // ./src/templates/loaders/test.css' 116 | var ___CSS_LOADER_API_IMPORT___ = __webpack_require__( 117 | './node_modules/css-loader/dist/runtime/api.js' 118 | ); 119 | exports = ___CSS_LOADER_API_IMPORT___(false); 120 | exports.push([ 121 | module.i, 122 | `html,body { 123 | background: #ccc; 124 | height: 100vh; 125 | } 126 | h1 { 127 | font-size: 100px; 128 | }`, 129 | '' 130 | ]); 131 | module.exports = exports; 132 | ``` 133 | 134 | 这段代码首先通过调用 `api.js` 初始化了 `exports` 并然后导出了 `css` 样式代码,下面我们看看 `api.js` 做了那些事: 135 | 136 | ```javascript 137 | // ./node_modules/css-loader/dist/runtime/api.js 138 | function(module, exports, __webpack_require__) { 139 | module.exports = function(useSourceMap) { 140 | var list = []; 141 | list.toString = function toString() { 142 | return this.map(function(item) { 143 | var content = cssWithMappingToString(item, useSourceMap); 144 | if (item[2]) { 145 | return '@media '.concat(item[2], ' {').concat(content, '}'); 146 | } 147 | return content; 148 | }).join(''); 149 | }; 150 | list.i = function(modules, mediaQuery, dedupe) { 151 | if (typeof modules === 'string') { 152 | modules = [[null, modules, '']]; 153 | } 154 | var alreadyImportedModules = {}; 155 | if (dedupe) { 156 | for (var i = 0; i < this.length; i++) { 157 | var id = this[i][0]; 158 | if (id != null) { 159 | alreadyImportedModules[id] = true; 160 | } 161 | } 162 | } 163 | for (var _i = 0; _i < modules.length; _i++) { 164 | var item = [].concat(modules[_i]); 165 | if (dedupe && alreadyImportedModules[item[0]]) { 166 | continue; 167 | } 168 | if (mediaQuery) { 169 | if (!item[2]) { 170 | item[2] = mediaQuery; 171 | } else { 172 | item[2] = ''.concat(mediaQuery, ' and ').concat(item[2]); 173 | } 174 | } 175 | list.push(item); 176 | } 177 | }; 178 | return list; 179 | }; 180 | 181 | function cssWithMappingToString(item, useSourceMap) { 182 | var content = item[1] || ''; 183 | var cssMapping = item[3]; 184 | if (!cssMapping) { 185 | return content; 186 | } 187 | if (useSourceMap && typeof btoa === 'function') { 188 | var sourceMapping = toComment(cssMapping); 189 | var sourceURLs = cssMapping.sources.map(function(source) { 190 | return '/*# sourceURL='.concat(cssMapping.sourceRoot || '').concat(source, ' */'); 191 | }); 192 | return [content] 193 | .concat(sourceURLs) 194 | .concat([sourceMapping]) 195 | .join('\n'); 196 | } 197 | return [content].join('\n'); 198 | } 199 | 200 | function toComment(sourceMap) { 201 | var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); 202 | var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,'.concat(base64); 203 | return '/*# '.concat(data, ' */'); 204 | } 205 | } 206 | ``` 207 | 208 | 上面代码可以看出 `css-loader` 主要提供了 `toString` 方法,将 `css` 文件导出为字符串。 若传入的 `useSourceMap` 为 `true`,则会生成并添加 `sourcemap` 到导出的字符串。默认不会生成 `sourcemap`,从导出后的代码 `exports = ___CSS_LOADER_API_IMPORT___(false);` 就可以看出。我们改动一下 `test-css.js`: 209 | 210 | ```javascript 211 | import style from './test.css'; 212 | console.log(style.toString()); 213 | ``` 214 | 215 | 输出的结果为: 216 | 217 | ```javascript 218 | html, 219 | body { 220 | background: #ccc; 221 | height: 100vh; 222 | } 223 | 224 | h1 { 225 | font-size: 100px; 226 | } 227 | ``` 228 | 229 | 现在我们改动一下 `webpack`,让 `css-loader` 生成 `sourcemap`: 230 | 231 | ```javascript 232 | use: [ 233 | { 234 | loader: 'css-loader', 235 | options: { 236 | sourceMap: true 237 | } 238 | } 239 | ]; 240 | ``` 241 | 242 | 现在 `toString` 输出的结果为: 243 | 244 | ```javascript 245 | html, 246 | body { 247 | background: #ccc; 248 | height: 100vh; 249 | } 250 | 251 | h1 { 252 | font-size: 100px; 253 | } 254 | 255 | /*# sourceURL=test.css */ 256 | /*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QuY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztFQUVFLGdCQUFnQjtFQUNoQixhQUFhO0FBQ2Y7O0FBRUE7RUFDRSxnQkFBZ0I7QUFDbEIiLCJmaWxlIjoidGVzdC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyJodG1sLFxuYm9keSB7XG4gIGJhY2tncm91bmQ6ICNjY2M7XG4gIGhlaWdodDogMTAwdmg7XG59XG5cbmgxIHtcbiAgZm9udC1zaXplOiAxMDBweDtcbn1cbiJdfQ== */ 257 | ``` 258 | 259 | 可以看出,现在内联了 `base64` 编码的 `json` 格式的 `sourcemap`,转码后是这样的: 260 | 261 | ```json 262 | { 263 | "version": 3, 264 | "sources": ["test.css"], 265 | "names": [], 266 | "mappings": "AAAA;;EAEE,gBAAgB;EAChB,aAAa;AACf;;AAEA;EACE,gBAAgB;AAClB", 267 | "file": "test.css", 268 | "sourcesContent": [ 269 | "html,\nbody {\n background: #ccc;\n height: 100vh;\n}\n\nh1 {\n font-size: 100px;\n}\n" 270 | ] 271 | } 272 | ``` 273 | 274 | ## 使用样式 275 | 276 | 目前虽然可以通过 `import` 导入 `css` 文件了,但是 `html` 还没有套用我们引入的样式。常用的可以衔接 `css-loader` 套用样式到 `html` 的方法有两种: 277 | 278 | 1. style-loader 279 | 2. mini-css-extract-plugin 280 | 281 | 首先是 `style-loader`,我们简单改动一下 `webpack` 配置: 282 | 283 | ```javascript 284 | { 285 | test: /\.css$/, 286 | use: ['style-loader', 'css-loader'] 287 | } 288 | ``` 289 | 290 | 可以看出,`loader` 是从右向左加载的。打包后,我们会发现源码中新增了一个 `module`: 291 | 292 | ```javascript 293 | // ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 294 | function(module, exports, __webpack_require__) { 295 | var isOldIE = (function isOldIE() { 296 | var memo; 297 | return function memorize() { 298 | if (typeof memo === 'undefined') { 299 | memo = Boolean(window && document && document.all && !window.atob); 300 | } 301 | return memo; 302 | }; 303 | })(); 304 | 305 | var getTarget = (function getTarget() { 306 | var memo = {}; 307 | return function memorize(target) { 308 | if (typeof memo[target] === 'undefined') { 309 | var styleTarget = document.querySelector(target); 310 | if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) { 311 | try { 312 | styleTarget = styleTarget.contentDocument.head; 313 | } catch (e) { 314 | styleTarget = null; 315 | } 316 | } 317 | memo[target] = styleTarget; 318 | } 319 | return memo[target]; 320 | }; 321 | })(); 322 | 323 | var stylesInDom = []; 324 | function getIndexByIdentifier(identifier) { 325 | var result = -1; 326 | for (var i = 0; i < stylesInDom.length; i++) { 327 | if (stylesInDom[i].identifier === identifier) { 328 | result = i; 329 | break; 330 | } 331 | } 332 | return result; 333 | } 334 | 335 | function modulesToDom(list, options) { 336 | var idCountMap = {}; 337 | var identifiers = []; 338 | for (var i = 0; i < list.length; i++) { 339 | var item = list[i]; 340 | var id = options.base ? item[0] + options.base : item[0]; 341 | var count = idCountMap[id] || 0; 342 | var identifier = ''.concat(id, ' ').concat(count); 343 | dCountMap[id] = count + 1; 344 | var index = getIndexByIdentifier(identifier); 345 | var obj = { 346 | css: item[1], 347 | media: item[2], 348 | sourceMap: item[3] 349 | }; 350 | if (index !== -1) { 351 | stylesInDom[index].references++; 352 | stylesInDom[index].updater(obj); 353 | } else { 354 | stylesInDom.push({ 355 | identifier: identifier, 356 | updater: addStyle(obj, options), 357 | references: 1 358 | }); 359 | } 360 | identifiers.push(identifier); 361 | } 362 | return identifiers; 363 | } 364 | 365 | function insertStyleElement(options) { 366 | var style = document.createElement('style'); 367 | var attributes = options.attributes || {}; 368 | if (typeof attributes.nonce === 'undefined') { 369 | var nonce = true ? __webpack_require__.nc : undefined; 370 | if (nonce) { 371 | attributes.nonce = nonce; 372 | } 373 | } 374 | Object.keys(attributes).forEach(function(key) { 375 | style.setAttribute(key, attributes[key]); 376 | }); 377 | if (typeof options.insert === 'function') { 378 | options.insert(style); 379 | } else { 380 | var target = getTarget(options.insert || 'head'); 381 | if (!target) { 382 | throw new Error( 383 | "Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid." 384 | ); 385 | } 386 | target.appendChild(style); 387 | } 388 | return style; 389 | } 390 | 391 | function removeStyleElement(style) { 392 | if (style.parentNode === null) { 393 | return false; 394 | } 395 | style.parentNode.removeChild(style); 396 | } 397 | 398 | var replaceText = (function replaceText() { 399 | textStore = []; 400 | return function replace(index, replacement) { 401 | textStore[index] = replacement; 402 | return textStore.filter(Boolean).join('\n'); 403 | }; 404 | })(); 405 | 406 | function applyToSingletonTag(style, index, remove, obj) { 407 | var css = remove 408 | ? '' 409 | : obj.media 410 | ? '@media '.concat(obj.media, ' {').concat(obj.css, '}') 411 | : obj.css; 412 | if (style.styleSheet) { 413 | style.styleSheet.cssText = replaceText(index, css); 414 | } else { 415 | var cssNode = document.createTextNode(css); 416 | var childNodes = style.childNodes; 417 | if (childNodes[index]) { 418 | style.removeChild(childNodes[index]); 419 | } 420 | if (childNodes.length) { 421 | style.insertBefore(cssNode, childNodes[index]); 422 | } else { 423 | style.appendChild(cssNode); 424 | } 425 | } 426 | } 427 | 428 | function applyToTag(style, options, obj) { 429 | var css = obj.css; 430 | var media = obj.media; 431 | var sourceMap = obj.sourceMap; 432 | if (media) { 433 | style.setAttribute('media', media); 434 | } else { 435 | style.removeAttribute('media'); 436 | } 437 | if (sourceMap && btoa) { 438 | css += '\n/*# sourceMappingURL=data:application/json;base64,'.concat( 439 | btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), 440 | ' */' 441 | ); 442 | } 443 | if (style.styleSheet) { 444 | style.styleSheet.cssText = css; 445 | } else { 446 | while (style.firstChild) { 447 | style.removeChild(style.firstChild); 448 | } 449 | style.appendChild(document.createTextNode(css)); 450 | } 451 | } 452 | 453 | var singleton = null; 454 | var singletonCounter = 0; 455 | function addStyle(obj, options) { 456 | var style; 457 | var update; 458 | var remove; 459 | if (options.singleton) { 460 | var styleIndex = singletonCounter++; 461 | style = singleton || (singleton = insertStyleElement(options)); 462 | update = applyToSingletonTag.bind(null, style, styleIndex, false); 463 | remove = applyToSingletonTag.bind(null, style, styleIndex, true); 464 | } else { 465 | style = insertStyleElement(options); 466 | update = applyToTag.bind(null, style, options); 467 | remove = function remove() { 468 | removeStyleElement(style); 469 | }; 470 | } 471 | update(obj); 472 | return function updateStyle(newObj) { 473 | if (newObj) { 474 | if ( 475 | newObj.css === obj.css && 476 | newObj.media === obj.media && 477 | newObj.sourceMap === obj.sourceMap 478 | ) { 479 | return; 480 | } 481 | update((obj = newObj)); 482 | } else { 483 | remove(); 484 | } 485 | }; 486 | } 487 | 488 | module.exports = function(list, options) { 489 | options = options || {}; 490 | if (!options.singleton && typeof options.singleton !== 'boolean') { 491 | options.singleton = isOldIE(); 492 | } 493 | list = list || []; 494 | var lastIdentifiers = modulesToDom(list, options); 495 | return function update(newList) { 496 | newList = newList || []; 497 | if (Object.prototype.toString.call(newList) !== '[object Array]') { 498 | return; 499 | } 500 | for (var i = 0; i < lastIdentifiers.length; i++) { 501 | var identifier = lastIdentifiers[i]; 502 | var index = getIndexByIdentifier(identifier); 503 | stylesInDom[index].references--; 504 | } 505 | var newLastIdentifiers = modulesToDom(newList, options); 506 | for (var _i = 0; _i < lastIdentifiers.length; _i++) { 507 | var _identifier = lastIdentifiers[_i]; 508 | var _index = getIndexByIdentifier(_identifier); 509 | if (stylesInDom[_index].references === 0) { 510 | stylesInDom[_index].updater(); 511 | stylesInDom.splice(_index, 1); 512 | } 513 | } 514 | lastIdentifiers = newLastIdentifiers; 515 | }; 516 | }; 517 | } 518 | ``` 519 | 520 | 分析上述代码可以得知,`style-loader` 暴露了一个方法,传入 `css-loader` 导出的 `list` 和自己的 `options`,主要通过 `insertStyleElement` 这一方法新建 `style` 标签并注入样式。所以这种方式 `css` 是以字符串的形式打包进 `js` 文件的。如果我想要单独导出 `css` 文件,就需要使用 `mini-css-extract-plugin` 了。 521 | 522 | 我们改动一下 `webpack` 配置: 523 | 524 | ```javascript 525 | const path = require('path'); 526 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 527 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 528 | 529 | module.exports = { 530 | mode: 'development', 531 | entry: path.join(__dirname, '../templates/loaders/test-css.js'), 532 | output: { 533 | path: path.join(__dirname, '../../dist'), 534 | filename: 'bundle-css.js' 535 | }, 536 | module: { 537 | rules: [ 538 | { 539 | test: /\.css$/, 540 | use: [ 541 | // 'style-loader', 542 | MiniCssExtractPlugin.loader, 543 | 'css-loader' 544 | ] 545 | } 546 | ] 547 | }, 548 | plugins: [ 549 | new HtmlWebpackPlugin(), 550 | new MiniCssExtractPlugin({ 551 | filename: '[name].css', 552 | chunkFilename: '[id].css' 553 | }) 554 | ], 555 | devServer: { 556 | contentBase: path.join(__dirname, '../../dist'), 557 | compress: true, 558 | port: 9000 559 | } 560 | }; 561 | ``` 562 | 563 | `mini-css-extract-plugin` 会从 `bundle` 包中抽出 `css` 文件,然后通过 `link` 标签引入外部样式。我们还可以通过 `optimize-css-assets-webpack-plugin` 对 `css` 文件进行压缩: 564 | 565 | ```javascript 566 | const path = require('path'); 567 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 568 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 569 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 570 | 571 | module.exports = { 572 | mode: 'development', 573 | entry: path.join(__dirname, '../templates/loaders/test-css.js'), 574 | output: { 575 | path: path.join(__dirname, '../../dist'), 576 | filename: 'bundle-css.js' 577 | }, 578 | module: { 579 | rules: [ 580 | { 581 | test: /\.css$/, 582 | use: [ 583 | // 'style-loader', 584 | MiniCssExtractPlugin.loader, 585 | 'css-loader' 586 | ] 587 | } 588 | ] 589 | }, 590 | plugins: [ 591 | new HtmlWebpackPlugin(), 592 | new MiniCssExtractPlugin({ 593 | filename: '[name].css', 594 | chunkFilename: '[id].css' 595 | }), 596 | new OptimizeCssAssetsPlugin() 597 | ], 598 | devServer: { 599 | contentBase: path.join(__dirname, '../../dist'), 600 | compress: true, 601 | port: 9000 602 | } 603 | }; 604 | ``` 605 | 606 | ## 相关文档 607 | 608 | - [Webpack 官方文档](https://www.webpackjs.com/loaders/css-loader/) 609 | --------------------------------------------------------------------------------