Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webpack 学习 #22

Open
nonocast opened this issue Mar 12, 2020 · 0 comments
Open

Webpack 学习 #22

nonocast opened this issue Mar 12, 2020 · 0 comments

Comments

@nonocast
Copy link
Owner

nonocast commented Mar 12, 2020

什么是webpack?

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

简单来说就是一个打包工具, 某种程度就是传统语言中的linker,所以只要你写javascript就不可能离开webpack。

当前webpack版本: 4.42.0

从最小化开始

yarn init -y 起步, 然后开一个src/index.js

index.js

console.log('hello world');

直接运行webpack, 默认entry为'./src',即./src/index.js, 默认output是./dist/main.js,然后就能生产一行超长的js。

默认情况下等同于如下:

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  }
};

文档中指出,首先需要了解几个核心概念:

Entry & Output

Entry就是webpack构建dependency Graph的起点,即程序入口。Output就是打包后的产物。

支持多entry,这个后面再说。

Loaders

Hey webpack compiler, when you come across a path that resolves to a '.txt' file inside of a require()/import statement, use the raw-loader to transform it before you add it to the bundle.

从entry开始,webpack会追踪所有依赖,但默认的webpack只跟进javascript和json的依赖,将其打包,webpack并不认识css, svg,所以需要loader来指引webpack如何转换(transform)

const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }

这里表示当依赖碰到'.txt'结尾的文件交由raw-loader进入webpack

Plugins

如果说loader是根据文件名来做分类处理,Plugins就如同koa/express中的middleware, 对整体输出做transform。

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

Mode

可以将Mode理解为webpack采用的configuration,默认是production

index.js

console.log(process.env.NODE_ENV);

直接运行NODE_ENV为undefined,通过webpack后运行输出'production',观察./dist/main.js,你会发现在bundle的过程中webpack直接将process.env.NODE_ENV替换为'production', 不再依赖于外部的环境变量。

NODE_ENV=test node ./dist/main

此时输出的依然是production。

除了对环境变量的影响,更多是采用的优化策略不同,具体见这里

看下面这句代码:

process.env.NODE_ENV = process.env.NODE_ENV || 'development';

首先肯定是不推荐去写process.env.NODE_ENV,但这句在非webpack下是没问题的,但是如果webpack后运行会报错,

"development" = "development" || false;

换句话说,写代码的时候需要考虑一下webpack的感受,否则怎么死的都不知道。

此外,__dirname会被替换为/, 而__filename的路径也变成/,所以这些都是打包后带来的副作用。

Browser Compatibility

webpack supports all browsers that are ES5-compliant (IE8 and below are not supported). webpack needs Promise for import() and require.ensure(). If you want to support older browsers, you will need to load a polyfill before using these expressions.

Node部分则是需要8.x以上即可。


在基本了解了这几个概念以后,我们尝试将mode改为development,你会得到的main.js如下

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("console.log(\"hello world\");\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

正好100行,强烈建议在debug一下这个100行,你大概就能理解一个基本的webpack输出的routine了,这个文件是不依赖任何外部文件就可以运行,webpack将node的require翻译成了自己的方式进行管理。

Q: 为什么将conole.log封装到eval中呢?

A: 添加sourcemap,通过devtool可以在运行bundle代码是定位到source file, 具体可以查看JavaScript Source Map 详解 - 阮一峰的网络日志

我们来增加一个内部依赖,

app.js

module.exports = {
  message: 'hello webpack'
}

index.js

const app = require('./app');
console.log(app.message);

webpack后的./dist/main.js

/******/ (function(modules) { // webpackBootstrap
/******/ 此处省略webpackBootstrap,每个输出都一样
/******/ ({

/***/ "./src/app.js":
/***/ (function(module, exports) {

eval("module.exports = {\n  message: 'hello webpack'\n}\n\n//# sourceURL=webpack:///./src/app.js?");

/***/ }),

/***/ "./src/index.js":
/***/ (function(module, exports, __webpack_require__) {

eval("const app = __webpack_require__(/*! ./app */ \"./src/app.js\");\n\nconsole.log(app.message);\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

很容易理解吧,再来看外部依赖,

index.js

const _ = require('lodash');
_.times(3, i => console.log(i));

main.js

/******/ (function(modules) { // webpackBootstrap
/******/ 省略
/******/ })
/************************************************************************/
/******/ ({

/***/ "../../../../usr/local/lib/node_modules/webpack/buildin/global.js":
/***/ (function(module, exports) {

eval("...//# sourceURL=webpack:///(webpack)/buildin/global.js?");

/***/ }),

/***/ "../../../../usr/local/lib/node_modules/webpack/buildin/module.js":
/***/ (function(module, exports) {

eval("...//# sourceURL=webpack:///(webpack)/buildin/module.js?");

/***/ }),

/***/ "./node_modules/lodash/lodash.js":
/*!***************************************!*\
  !*** ./node_modules/lodash/lodash.js ***!
  \***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("...//# sourceURL=webpack:///./node_modules/lodash/lodash.js?");

/***/ }),

/***/ "./src/index.js":
/***/ (function(module, exports, __webpack_require__) {

eval("const _ = __webpack_require__(/*! lodash */ \"./node_modules/lodash/lodash.js\");\n\n// _.times(3, i => console.log(i));\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

先不说原理,看结果来说,就是webpack碰到外部lodash时,这时会先导入自身的globals和modules两个模块,然后导入lodash, 最后导入index.js

这样的好处是生成的js解决了所有依赖问题,直接复制到服务器上就可以运行,坏处也很明显,就是当文件多了以后这个文件要爆炸,单单是加了lodash就从3.8K变为552K, production为62K,看情况取舍。

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies), 这时候就需要用到externals配置。

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  externals: {
    lodash: 'commonjs lodash'
  }
};

此时生成main.js

{

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("const _ = __webpack_require__(/*! lodash */ \"lodash\");\n_.times(3, i => console.log(i));\n\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ }),

/***/ "lodash":
/*!*************************!*\
  !*** external "lodash" ***!
  \*************************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("module.exports = require(\"lodash\");\n\n//# sourceURL=webpack:///external_%22lodash%22?");
}

你会发现其实外部依赖就是直接利用node的require进行动态加载。

我试了一下,也是OK的,其实就是一个意思,当碰到lodash时如何处理,

externals: {
  lodash: "require('lodash')",
}

当你希望排除所有依赖包的时候,你不可能一个个来写,所以可以直接利用externals-dependencies, 源代码只有21行,

import fs from 'fs'
import path from 'path'
export default function (arr = []){
    var packageJson,externals = {};
    try {
        var packageJsonString = fs.readFileSync(path.join(process.cwd(), './package.json'), 'utf8');
        packageJson = JSON.parse(packageJsonString);
    } catch (e){
        throw 'can not find package.json'
    }
    var sections = ['dependencies'].concat(arr);
    sections = new Set(sections)

    var deps = {};
    sections.forEach(function(section){
        Object.keys(packageJson[section] || {}).forEach(function(dep){
            externals[dep] = 'commonjs ' + dep;
        });
    });
    return externals
}

实质就是遍历所有node_modules然后返回一个array, pkg => 'commonjs pkg'。

官网上也说externals更多是服务library developers, 避免重复加载相同的依赖,我感觉如果是node落地服务,更多的倾向全量打包,一个js搞定,很多时候服务器是没有网络无法做npm install,这个时候就特别有价值。

到这里我们就可以玩一个花出来,比如说在bundle后如何混合加载unbundle的javascript file,

externals: {
  lodash: "commonjs lodash",
  plugins: "require('./plugins')"
}

index.js

const plugins = require('plugins');
console.log(plugins);

./dist/plugins/index.js

module.exports = [
  'plugin A',
  'plugin B'
]

再来,在Module Resolution中指出webpack会通过分析require/import指令来构建dependency graph,但是支持的require也是有要求的:

  • Absolute paths: require('/path/to/module');
  • Relative paths: require('./module');
  • Module paths: require('lodash');

比如说 index.js

let x = './app';
const app = require(x);
console.log(app.message);

node直接运行没问题,但是webpack后运行报错Error: Cannot find module './app'

但是,

let x = 'app';
const app = require('./' + x);
console.log(app.message);

这样就ok,原因是require不能单单是变量,一定要有一部分字符串才能引导进require.context方式。

如果想加载多个文件,

const fs = require('fs');
const path = require('path');

['card1', 'card2'].forEach(file => {
  console.log(require('./cards/' + file));
});

如果是目录对应

const fs = require('fs');
const path = require('path');

fs.readdir('./src/cards', (error, files) => {
  if (error) throw error;
  files.forEach(file => {
    console.log(require('./cards/' + file));
  });
});

EOF

参考:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant