前言
主要將結合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)