前端模塊化二——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)
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息