前言
主要将结合webpack实际项目来说解这些规范,包括webpack项目中用到的NodeJS模块的其路径分析、文件定位、模块解析。
以及webpack对CommonJS、AMD、CMD、ES6模块化的支持状况、使用。
webpack 项目中的模块化
第一篇文章全部的讲解大多都是基于规范来说的,可是在实际开发中,咱们都是结合webpack来使用的。
咱们知道webpack是一个打包工具,将咱们的代码打包成一个或多个模块,最后这些被打包的模块会被插入到对应HTML模板里面,供浏览器中使用。
或者说,webpack实际上是一个前端的打包工具,webpack的entry就是用来配置打包的模块,实际是告诉webpack将哪些文件打成一个或多个包。而且webpack会构建一个依赖关系图,将入口文件的全部依赖文件都会打进这个包里。
在整个webpack项目中,其实CommonJS、AMD、CMD、ES6模块化均可以用到。
下面咱们根据vue-cli的项目来结合实战来给你们说明webpack项目中所涉及的全部模块化。
npm install -g @vue/cli-init
vue init webpack my-project复制代码
vue-cli 生成的vue项目,其目录结构以下:
其实平时咱们基于webpack的项目大体如此,项目里的模块化有涉及Nodejs的模块化、ES六、AMD、webpack模块。
下面咱们详细讲解,基于两点Nodejs模块,和webpack下的模块。
Nodejs模块化
./build文件夹下,其实都是Node代码,通常写一些webpack的配置及Node运行的脚本。
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
...复制代码
其实这就是一个标准的Node模块,当在Node模块里require的时候就会根据Node模块的路径分析、文件定位、编译执行来加载模块。举例说明:
-
path——Node的核心模块,由于核心模块早已在Node编译时就编译成二进制文件存入内存了,因此path模块直接从内存加载,比通常模块加载速度快。
-
ora ——其既不是Node的核心模块,又不是以.、..和/开头的绝对路径或者相对路径,所以Node就会到前面所说的模块路径里去寻找,会首先在当前目录的node_modules里去寻找,由于咱们require一个模块通常会在当前项目里使用npm 或者yarn安装所使用模块的npm包,因此通常在当前node_modules目录下就能找到,而后作一系列的文件定位、扩展名分析等,找到模块的入口文件,而后将模块执行一遍生成一个对象的拷贝放在内存里,以便之后的加载。
-
../config——相对定位形式require,一看就知道是普通的文件模块,就是项目里咱们本身建立的文件模块,也会通过路径分析、文件定位、编译执行三个步骤,执行也是返回一个对象的拷贝存在内存里,以便之后的加载。
以上代码咱们还能够看到其实webpack也是做为一个Node模块引入项目中的。
那咱们再看看webpack.base.conf.js模块,这个文件自己也是一个Node模块,前面已经说过了,Node中一个文件就是一个模块。
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
...
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
...
]
},
...
}复制代码
能够看到项目里并无定义 module 和__dirname却直接使用了,咱们能够将其打印出来,打印结果。
//console.log(module)结果
Module {
id: '/Users/baidu/daisy/demos/vue-cli/my-project/build/webpack.base.conf.js',
exports: {},
parent:
Module {
id: '/Users/baidu/daisy/demos/vue-cli/my-project/build/webpack.dev.conf.js',
exports: {},
parent:
Module {
id: '/Users/baidu/daisy/demos/vue-cli/my-project/node_modules/webpack/bin/convert-argv.js',
exports: [Function],
parent: [Object],
filename: '/Users/baidu/daisy/demos/vue-cli/my-project/node_modules/webpack/bin/convert-argv.js',
loaded: true,
children: [Array],
paths: [Array] },
filename: '/Users/baidu/daisy/demos/vue-cli/my-project/build/webpack.dev.conf.js',
loaded: false,
children: [ [Object], [Object], [Object], [Object], [Circular] ],
paths:
[ '/Users/baidu/daisy/demos/vue-cli/my-project/build/node_modules',
'/Users/baidu/daisy/demos/vue-cli/my-project/node_modules',
'/Users/baidu/daisy/demos/vue-cli/node_modules',
'/Users/baidu/daisy/demos/node_modules',
'/Users/baidu/daisy/node_modules',
'/Users/baidu/node_modules',
'/Users/node_modules',
'/node_modules' ] },
filename: '/Users/baidu/daisy/demos/vue-cli/my-project/build/webpack.base.conf.js',
loaded: false,
children:
[ Module {
id: '/Users/baidu/daisy/demos/vue-cli/my-project/build/utils.js',
exports: [Object],
parent: [Object],
filename: '/Users/baidu/daisy/demos/vue-cli/my-project/build/utils.js',
loaded: true,
children: [Array],
paths: [Array] },
Module {
id: '/Users/baidu/daisy/demos/vue-cli/my-project/config/index.js',
exports: [Object],
parent: [Object],
filename: '/Users/baidu/daisy/demos/vue-cli/my-project/config/index.js',
loaded: true,
children: [],
paths: [Array] },
Module {
id: '/Users/baidu/daisy/demos/vue-cli/my-project/build/vue-loader.conf.js',
exports: [Object],
parent: [Circular],
filename: '/Users/baidu/daisy/demos/vue-cli/my-project/build/vue-loader.conf.js',
loaded: true,
children: [Array],
paths: [Array] } ],
paths:
[ '/Users/baidu/daisy/demos/vue-cli/my-project/build/node_modules',
'/Users/baidu/daisy/demos/vue-cli/my-project/node_modules',
'/Users/baidu/daisy/demos/vue-cli/node_modules',
'/Users/baidu/daisy/demos/node_modules',
'/Users/baidu/daisy/node_modules',
'/Users/baidu/node_modules',
'/Users/node_modules',
'/node_modules' ] } 复制代码
我实际上是这么理解的,就是Module 类的一个实例对象,前面打印的模块路径就是module.paths , 因此module就是表明Module后面的那个字面量对象。
//console.log(__dirname)
/Users/baidu/daisy/demos/vue-cli/my-project/build复制代码
咱们之因此能够直接使用module和__dirname是由于,前面所说Node对js模块的编译会将其首尾包装,包装以后以下:
(function(exports, require, module, __filename, __dirname){
//webpack.base.conf.js 内容
...
});复制代码
所以Node中每个文件就是一个模块,而且每一个模块都有exports, require, module, __filename, __dirname这些变量能够直接使用的。
webpack模块化支持状况
咱们知道webpack是根据entry配置的入口来打包,因此项目中的业务逻辑代码都要先通过webpack这一层,其实webpack也有模块化。
咱们先来看看webpack的模块化,如下是webpack4对模块化的描述。
"Node.js 从最一开始就支持模块化编程。然而,在 web,模块化的支持正缓慢到来。在 web 存在多种支持 JavaScript 模块化的工具,这些工具各有优点和限制。webpack 基于从这些系统得到的经验教训,并将_模块_的概念应用于项目中的任何文件。"
webpack1——的时候须要使用特定的loader来转换ES2015(ES6) 的import,
webpack2—— 默认支持ES2015的import了。
webpack3—— 默认支持 javascript/auto模块类型, 所谓的javascript/auto模块类型是指支持全部的JS模块规范——CommonJS、AMD、ES6
也就是说webpack3就已经彻底默认支持CommonJS、AMD/CMD、ES6模块规范,开箱即用。
webpack4——支持5种模块类型(type),在webpack4.0.0release时有说明。
-
javascript/auto: (webpack 3中的默认类型)支持全部的JS模块系统:CommonJS、AMD/CMD、ESM
-
javascript/auto: EcmaScript 模块,在其余的模块系统中不可用(默认 .mjs 文件)
-
javascript/dynamic: 仅支持 CommonJS & AMD,EcmaScript 模块不可用
-
json: 可经过 require 和 import 导入的 JSON 格式的数据(默认为 .json 的文件)
-
webassembly/experimental: WebAssembly 模块(处于试验阶段,默认为 .wasm 的文件)
PS(>.<吐槽官方文档): 虽然在官方文档或者在相关资料上并无找到是否支持CMD规范的说明,可是经过在webpack4(4.16.3)实际测试中发现,webpack4也是默认支持CMD规范的。
而这5种模块类型在项目里实际是怎么区分的呢,4.0.0release时原文这么说的。
Module type is automatically choosen for mjs, json and wasm extensions. Other extensions need to be configured via module.rules[].type
大体意思是webpak4会自动解析.wasm,.mjs,.js和.json的文件,可是其余扩展名的文件须要在 module.rules[].type 中配置,配置以下:
module: {
rules:[{
type: 'javascript/auto',
test: /\.(json)/,
exclude: /(node_modules|bower_components)/,
}]
}复制代码
type的值能够是:javascript/auto、javascript/dynamic、javascript/auto、json、webassembly/experimental 5种类型,分别表明上面所说的5种模块类型。
因此综上所述,前面所讲的所有的前端模块规范,包括CommonJS在webpack4都是默认支持的,也就是说入口文件包括其依赖文件中所有均可以使用这些规范(CommonJS,AMD、CMD、ES6模块化),而且开箱即用,不须要额外的配置。
webpack模块解析
经过阅读webpack4的官方文档,能够发现webpack的模块解析规则和Nodejs很是类似。
webpack使用enhanced-resolve 来解析文件路径,支持三种路径形式
模块路径支持配置,在resolve.modules里配置,以下所示
module.exports = {
...
resolve: {
modules: [
"node_modules",
path.resolve(__dirname, "src")
],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'#': resolve('widget'),
},
extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
}
...
}复制代码
还能够经过resolve.alias来配置一个模块的别名,如上所示,配置完以后
import vue from 'vue$';
//等价于
import vue from 'vue/dist/vue.esm.js';复制代码
ps:因此若是项目里配置了resolve.extensions ,import或者require时就能省略扩展名,webpack将根据resolve.extensions中的配置来自动匹配扩展名。
(2)若是路径指向一个文件夹,相似于Nodejs文件夹会被当作一个包来解析,第一步是查找package.json文件:
module.exports = {
...
resolve: {
...
mainFields: ["browser", "module", "main"]
...
}
...
}复制代码
module.exports = {
...
resolve: {
...
mainFiles: ["index"]
...
}
...
}
复制代码
就是告诉webpack将包中的index文件做为入口,再进行扩展名匹配。
webpack 对于各类模块规范的模块是如何解析的呢?支持程度彻底符合规范吗?
//utils.js
var age = 18;
module.exports = {
age,
addAge: ()=>{
age++;
}
}
//index.js
let person = require('./util.js');
console.log(person.age);
person.addAge();
console.log(age);
//输出
18
18复制代码
能够发现CommonJS 、AMD、CMD确实都是运行时加载,而且加载的都是第一次运行时返回的对象的拷贝,模块内值的改变,再次加载的模块对象也不会受影响。
这里重点结合实际看webpack下的——ES6模块化的静态引入(只引入{}中的API)——支持状况。
//greeting.js
export function sayHi() {
console.log('Hi');
}
export function sayBye() {
console.log('Bye');
}
复制代码
//src/index.js
...
import{ sayHi } from './greeting';
...复制代码
将build/webpack.prod.conf.js文件中的mode改成development
//webpack.prod.conf.js
mode: 'development',复制代码
为了看出代码的打包状况,将mode改成development,build的时候就不会将代码压缩
npm run build 复制代码
会在项目根目录生成dist文件夹,结构以下(文件结构根据配置生成的)
咱们主要看dist/static/js/index.js模块,由于本项目只配置了一个入口文件就是src/index.js,
vendor.js是依赖的npm包打成的一个文件,具体配置参考项目源码。在dist/static/js/index.js中看到整个greeting.js模块都加载了,可是其实按照ES6规范里说的,只引入了sayHi,就应该只加载sayHi方法,可是看到其实没有引入的sayBye方法也加载了。
图3 没有tree shaking的es6没有彻底实现静态引入
webpack4 tree shaking
(1)项目的package.json添加sideEffects配置
{
"name": "webpack-demo",
"sideEffects": [
"*.less",
"*.css",
]
}
复制代码
由于treeShaking能够删除文件中未使用的部分。须要配置sideEffects属性告诉webpack哪些文件能够安全treeShaking,没有反作用,可是项目里用到相似css-loader并导入css/less,就须要在sideEffects中配置,代表.css,.less文件不该用treeShaking。
由于webpack的tree shaking 依赖uglifyjs将dead code去掉,webpack4 mode 为production时默认启动uglifyjs
(3)npm run build以后查看dist/static/js/index.js, 格式化以后,发现'Bye'字符串再也找不到了,说明sayBye方法彻底去掉了。相关代码如图:
//webpack.prod.conf.js
var baseWebpackConfig = require('./webpack.base.conf');
...
const UglifyJS = require('uglify-es');
const DefaultUglifyJsOptions = UglifyJS.default_options();
const compress = DefaultUglifyJsOptions.compress;
for(let compressOption in compress) {
compress[compressOption] = false;
}
compress.unused = true;
var webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
...
optimization: {
splitChunks: {
name: true,
chunks: 'all',
},
minimize: true,
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
compress,
mangle: false,
output: {
beautify: true
}
},
}),
],
}
...
}
...复制代码
再npm run build,能够看到以下结果,代码已经格式化,不须要再借助vscode手动格式化
图5 没有compress 的tree shaking 效果
咱们能够更直观的看到tree shaking以后的效果没有引入的sayBye方法被彻底移除了。
tree shaking 踩坑:以前各类尝试tree shaking 都没法成功,最后发现是由于添加了
// .babelrc
{
"plugins": ["react-hot-loader/babel"]
}复制代码
这是由于项目里使用了react-hot-loader,其要求添加如上代码,奇怪的是,将以上代码删除,不只能够成功tree shaking 还不影响 react 的hot-reload时的状态保存。
总结
前端模块化,话其规范,种类繁多,包括CommonJS/ AMD/CMD/ES6模块化,通晓所有方能成器。webpack4都已经默认支持,无需额外配置,可是ES6的静态模块引入须要配合tree shaking方能实现。
参考
- github.com/webpack/web…
- Webpack 4 不彻底迁移指北 · Issue #60 · dwqs/blog
- webpack 中文文档(@印记中文) https://docschina.org/
- ECMAScript 6入门
- wanago.io/2018/08/13/…
- 朴灵 (2013) 深刻浅出Node.js. 人民邮电出版社, 北京。
- JavaScript 标准参考教程(alpha)