前端模块化二——webpack项目中的模块化

前言

本文章为第一篇关于前端模块化 前端模块化一——规范详述的续篇。
主要将结合webpack实际项目来说解这些规范,包括webpack项目中用到的NodeJS模块的其路径分析、文件定位、模块解析。
以及webpack对CommonJS、AMD、CMD、ES6模块化的支持状况、使用。
还有webpack对ES6模块静态加载的支持。
webpack tree shaking。

webpack 项目中的模块化


第一篇文章全部的讲解大多都是基于规范来说的,可是在实际开发中,咱们都是结合webpack来使用的。
咱们知道webpack是一个打包工具,将咱们的代码打包成一个或多个模块,最后这些被打包的模块会被插入到对应HTML模板里面,供浏览器中使用。
或者说,webpack实际上是一个前端的打包工具,webpack的entry就是用来配置打包的模块,实际是告诉webpack将哪些文件打成一个或多个包。而且webpack会构建一个依赖关系图,将入口文件的全部依赖文件都会打进这个包里。
在整个webpack项目中,其实CommonJS、AMD、CMD、ES6模块化均可以用到。

下面咱们根据vue-cli的项目来结合实战来给你们说明webpack项目中所涉及的全部模块化。
安装vue-cli 2
npm install -g @vue/cli-init

vue init webpack my-project复制代码
vue-cli 生成的vue项目,其目录结构以下:
图1 vue-cli 2 生成的项目结构
其实平时咱们基于webpack的项目大体如此,项目里的模块化有涉及Nodejs的模块化、ES六、AMD、webpack模块。
下面咱们详细讲解,基于两点Nodejs模块,和webpack下的模块。

Nodejs模块化

./build文件夹下,其实都是Node代码,通常写一些webpack的配置及Node运行的脚本。
咱们看./build/build.js文件
'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后面的那个字面量对象。
再将__dirname打印出来。
//console.log(__dirname)

/Users/baidu/daisy/demos/vue-cli/my-project/build复制代码
__dirname 就是当前文件所在的目录。

咱们之因此能够直接使用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 基于从这些系统得到的经验教训,并将_模块_的概念应用于项目中的任何文件。"
webpack支持各类方式表达的模块依赖关系。
  • ES2015 import 语句
  • CommonJS require语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的@import 语句
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种模块类型。

通在webpack4.16.3项目中尝试,发现javascript/auto模块类型是默认支持的,不须要配置type,能够参考个人 webpack4.16.3react项目

因此综上所述,前面所讲的所有的前端模块规范,包括CommonJS在webpack4都是默认支持的,也就是说入口文件包括其依赖文件中所有均可以使用这些规范(CommonJS,AMD、CMD、ES6模块化),而且开箱即用,不须要额外的配置。

webpack模块解析

经过阅读webpack4的官方文档,能够发现webpack的模块解析规则和Nodejs很是类似。
webpack使用enhanced-resolve 来解析文件路径,支持三种路径形式
  • 绝对路径——绝对路径不须要进一步路径解析
  • 相对路径——import/require中给定的相对路径,会添加此上下文路径(context path),以产生模块的绝对路径(absolute path)。
  • 模块路径——相似Nodejs的模块路径。
模块路径支持配置,在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';复制代码
路径解析后,解析器将检查路径是否指向文件或目录。
(1)若是路径指向一个文件:
  • 若是路径具备文件扩展名,则被直接将文件打包。
  • 不然,将使用 [resolve.extensions] 选项做为文件扩展名来解析,此选项告诉解析器在解析中可以接受哪些扩展名(例如 .js, .jsx)
ps:因此若是项目里配置了resolve.extensions ,import或者require时就能省略扩展名,webpack将根据resolve.extensions中的配置来自动匹配扩展名。
(2)若是路径指向一个文件夹,相似于Nodejs文件夹会被当作一个包来解析,第一步是查找package.json文件:
  • 若是包含package.json,则按顺序查找 resolve.mainFields配置选项中指定的字段。
resolve.mainFields——实际上是告诉webpack要把package.json中哪一个属性定义的文件路径当作包的入口文件,其配置以下:
module.exports = {
  ...
  resolve: {
   ...
   mainFields: ["browser", "module", "main"]
   ...
  }
  ...
}复制代码
  • 若是不包含package.json 或者package.json中的main字段没有返回一个有效路径,则按照顺序查找 resolve.mainFiles(注意和resolve.mainFields区分) 配置选项中指定的文件名。 resolve.mainFields的配置以下:
module.exports = {
  ...
  resolve: {
   ...
   mainFiles: ["index"]
   ...
  }
  ...
}


复制代码
就是告诉webpack将包中的index文件做为入口,再进行扩展名匹配。

webpack 对于各类模块规范的模块是如何解析的呢?支持程度彻底符合规范吗?

对于CommonJS、AMD、CMD通代相似下面的代码来测试(具体查看源码 webpack4.16.3react项目
//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文件,内容以下:
//greeting.js
export function sayHi() {
  console.log('Hi');
}
export function sayBye() {
  console.log('Bye');
}


复制代码
在项目src/index.js里引入
//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文件夹,结构以下(文件结构根据配置生成的)
图2 vue-cli项目结构图
咱们主要看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没有彻底实现静态引入
咱们再给项目加上webpack的treeShaking(参考 tree shaking | webpack 中文网

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。
(2)mode改成production
由于webpack的tree shaking 依赖uglifyjs将dead code去掉,webpack4 mode 为production时默认启动uglifyjs
(3)npm run build以后查看dist/static/js/index.js, 格式化以后,发现'Bye'字符串再也找不到了,说明sayBye方法彻底去掉了。相关代码如图:
图4 tree shaking 以后
为了更清楚看看tree shaking以后的效果,我手动配置了一下uglifyjs,关掉compress 等功能,并格式化,配置以下:(具体查看源码 webpack4.16.3react项目
//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方能实现。

参考

  1. github.com/webpack/web…
  2.  Webpack 4 不彻底迁移指北 · Issue #60 · dwqs/blog
  3.  webpack 中文文档(@印记中文) https://docschina.org/
  4. ECMAScript 6入门
  5.  wanago.io/2018/08/13/…
  6. 朴灵 (2013) 深刻浅出Node.js. 人民邮电出版社, 北京。
  7. JavaScript 标准参考教程(alpha)
相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息