有時候是不是會有這樣的疑問:紛繁的功能文件,到最後是怎麼組合成起來並且在瀏覽器中展示的?為什麼需要 node 環境?下面這篇文章要跟大家介紹一下node是怎麼把多個JS模組連結在一起的?希望對大家有幫助!
瀏覽器本身只能做一些展示及使用者互動的功能,對於系統操作的能力很有限,那麼,瀏覽器內建的運作環境顯然不滿足一些更為人性化的開發模式,例如:更好的區分功能模組、實作檔案的操作。那麼,帶來的缺陷就很明顯,例如:各個JS 檔案比較分散,需要在html 頁面裡面單獨引入,如果某個JS 檔案需要其他的JS 函式庫,那麼很可能會因為html 頁面未引入而報錯,在功能龐大的專案裡,手動的管理這些功能文件確實讓人有點捉襟見肘。
那麼, node 到底是怎麼更友善的提供開發的呢?其實,上面也說了,人為的管理文件依賴不但會消耗大量的精力,還會存在疏漏,那麼,是不是以用自動化的方式進行管理就會好很多?是的,在node 的運作環境裡拓寬了對系統的操作能力,也就是說,或許以前開發者也想透過一些程式碼來完成那些機械瑣碎的工作,但是,只有想法沒有操作權限,最後只能望洋興嘆。現在,可以以node 的一些擴充功能對文件進行先前加工與整理,再添加一些自動化的程式碼,最後轉換為一個瀏覽器可識別的、完整的JS 文件,這樣一來,多個文件的內容,便可以匯集到一個文件。 【相關教學推薦:nodejs影片教學、程式設計教學】
先建立一些JS文件,如下圖所示:
這些文件都是手動創建,babel-core 這個文件是從全域的node_modules 裡面複製出來的,如下圖:
為什麼要複製出來?這是因為,任何腳手架幹的事其實都是為了快速搭建,但是,怎麼能理解它幹的什麼事呢?那乾脆就直接複製吧,本身,node 除了一些內建的模組,其他的都需要透過指明require 路徑的方式來找到相關模組,如下圖所示:
透過require('./babel-core') 方法,解析一個函式模組下的方法。
1、寫入口文件,轉換ES6程式碼
#entrance.js 作為入口文件,作用就是設定工作從哪開始?怎麼開始?那麼,這裡的工作指的就是轉換ES6程式碼,以提供瀏覽器使用。
//文件管理模块 const fs = require('fs'); //解析文件为AST模块 const babylon = require('babylon'); //AST转换模块 const { transformFromAst } = require('./babel-core'); //获取JS文件内容 let content = fs.readFileSync('./person.js','utf-8') //转换为AST结构,设定解析的文件为 module 类型 let ast = babylon.parse(content,{ sourceType:'module' }) //将ES6转换为ES5浏览器可识别代码 le t { code } = transformFromAst(ast, null, { presets: ['es2015'] }); //输出内容 console.log('code:\n' + `${code}`)
上面的程式碼很簡單,最終的目的就是將 module 類型的 person.js 檔案轉換為 ES5。
let person = {name:'wsl'} export default person
終端運行入口文件,如下所示:
node entrance.js
列印一下程式碼,如下圖所示:
"use strict"; //声明了一个 __esModule 为 true 的属性 Object.defineProperty(exports, "__esModule", { value: true }); var person = { name: 'wsl' }; exports.default = person;
可以,看到列印的程式碼,裡面都是瀏覽器能辨識的程式碼,依照常理,看看能不能直接運作一下?
下面將這段程式碼透過fs 功能寫入一個js 檔案並讓一個頁面引用,來看看效果:
fs.mkdir('cache',(err)=>{ if(!err){ fs.writeFile('cache/main.js',code,(err)=>{ if(!err){ console.log('文件创建完成') } }) } })
再執行指令,如圖所示:
瀏覽器運行結構,如圖所示:
##其實程式碼生成完就有很明顯的錯誤,未宣告變量,怎麼會不報錯呢?這時候,在入口文件輸入之前就需要添加一些自訂輔助代碼,來解決一下這個報錯。 解決的方式也很簡單,將原始code 的未宣告的 exports 變數透過自執行函數的方式包裝一下,再傳回指定物件。
//完善不严谨的code代码 function perfectCode(code){ let exportsCode = ` var exports = (function(exports){ ${code} return exports })({}) console.log(exports.default)` return exportsCode } //重新定义code code = perfectCode(code)
main.js 檔案
var exports = (function(exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var person = { name: 'wsl' }; exports.default = person; return exports })({}) console.log(exports.default)
现在浏览器运行正常了。
2、处理 import 逻辑
既然是模块,肯定会存在一个模块依赖另一个或其他很多个模块的情况。这里先不着急,先看看person 模块引入单一 animal 模块后的代码是怎样的?
animal 模块很简单,仅仅是一个对象导出
let animal = {name:'dog'} export default animal
person 模块引入
import animal from './animal' let person = {name:'wsl',pet:animal} export default person
看下转换后的代码
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _animal = require("./animal"); var _animal2 = _interopRequireDefault(_animal); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var person = { name: 'wsl', pet: _animal2.default }; exports.default = person;
可以看到,转换后会多一个未声明的 require 方法,内部声明的 _interopRequireDefault 方法已声明,是对 animal 导出部分进行了一个包裹,让其后续的代码取值 default 的时候保证其属性存在!
下面就需要对 require 方法进行相关的处理,让其转为返回一个可识别、可解析、完整的对象。
是不是可以将之前的逻辑对 animal 模块重新执行一遍获取到 animal 的代码转换后的对象就行了?
但是,这里先要解决一个问题,就是对于 animal 模块的路径需要提前获取并进行代码转换,这时候给予可以利用 babel-traverse 工具对 AST 进行处理。
说到这里,先看一下 JS 转换为 AST 是什么内容?
这里简单放一张截图,其实是一个 JSON 对象,存储着相关的代码信息,有代码位置的、指令内容的、变量的等等。
拿到它的目的其实就是找到import 对应节点下的引入其他模块的路径
通过 babel-traverse 找到 AST 里面 import 对应的信息
const traverse = require('babel-traverse').default; //遍历找到 import 节点 traverse(ast,{ ImportDeclaration:({ node })=>{ console.log(node) } })
输出看下节点打印的结构
Node { type: 'ImportDeclaration', start: 0, end: 29, loc: SourceLocation { start: Position { line: 1, column: 0 }, end: Position { line: 1, column: 29 } }, specifiers: [ Node { type: 'ImportDefaultSpecifier', start: 7, end: 13, loc: [SourceLocation], local: [Node] } ], source: Node { type: 'StringLiteral', start: 19, end: 29, loc: SourceLocation { start: [Position], end: [Position] }, extra: { rawValue: './animal', raw: "'./animal'" }, value: './animal' } }
可以看到 node.source.value 就是 animal 模块的路径,需要的就是它。
扩展入口文件功能,解析 import 下的 JS 模块,
添加 require 方法
//完善代码 function perfectCode(code){ let exportsCode = ` //添加require方法 let require = function(path){ return {} } let exports = (function(exports,require){ ${code} return exports })({},require) ` return exportsCode }
这样转换完的 main.js 给不会报错了,但是,这里需要解决怎么让 require 方法返回 animal 对象
let require = function(path){ return {} } let exports = (function(exports,require){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _animal = require("./animal"); var _animal2 = _interopRequireDefault(_animal); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var person = { name: 'wsl', pet: _animal2.default }; exports.default = person; return exports })({},require)
下面就需要添加 require 方法进行 animal 对象的返回逻辑
//引入模块路径 let importFilesPaths = [] //引入路径下的模块代码 let importFilesCodes = {} //获取import节点,保存模块路径 traverse(ast,{ ImportDeclaration:({ node })=>{ importFilesPaths.push(node.source.value) } }) //解析import逻辑 function perfectImport(){ //遍历解析import里面对应路径下的模块代码 importFilesPaths.forEach((path)=>{ let content = fs.readFileSync(path + '.js','utf-8') let ast = babylon.parse(content,{ sourceType:'module' }) let { code } = transformFromAst(ast, null, { presets: ['es2015'] }); //转换code code = perfectImportCode(code) importFilesCodes[path] = code }) } //完善import代码 function perfectImportCode(code){ let exportsCode = `( function(){ let require = function(path){ let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})() return exports } return (function(exports,require){${code} return exports })({},require) } )() ` return exportsCode } //完善最终输出代码 function perfectCode(code){ let exportsCode = ` let require = function(path){ let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})() return exports } let exports = (function(exports,require){ ${code} return exports })({},require) console.log(exports.default) ` return exportsCode }
上面的代码其实没有什么特别难理解的部分,里面的自执行闭包看着乱,最终的目的也很清晰,就是找到对应模块下的文件 code 代码进行自运行返回一个对应的模块对象即可。
看下转换后的 main.js 代码
let require = function(path){ let exports = (function(){ return eval({"./animal":"(\n function(){\n let require = function(path){\n let exports = (function(){ return eval({}[path])})()\n return exports\n }\n return (function(exports,require){\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar animal = { name: 'dog' };\n\nexports.default = animal; \n return exports\n })({},require)\n }\n )()\n "}[path])})() return exports } let exports = (function(exports,require){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _animal = require("./animal"); var _animal2 = _interopRequireDefault(_animal); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var person = { name: 'wsl', pet: _animal2.default }; exports.default = person; return exports })({},require) console.log(exports.default)
刷新浏览器,打印结果如下:
可以看到,pet 属性被赋予了新值。
const fs = require('fs'); const babylon = require('babylon'); const traverse = require('babel-traverse').default; const { transformFromAst } = require('./babel-core'); //解析person文件 let content = fs.readFileSync('./person.js','utf-8') let ast = babylon.parse(content,{ sourceType:'module' }) //引入模块路径 let importFilesPaths = [] //引入路径下的模块代码 let importFilesCodes = {} //保存import引入节点 traverse(ast,{ ImportDeclaration:({ node })=>{ importFilesPaths.push(node.source.value) } }) //person.js 对应的code let { code } = transformFromAst(ast, null, { presets: ['es2015'] }); //解析import逻辑 function perfectImport(){ importFilesPaths.forEach((path)=>{ let content = fs.readFileSync(path + '.js','utf-8') let ast = babylon.parse(content,{ sourceType:'module' }) let { code } = transformFromAst(ast, null, { presets: ['es2015'] }); code = perfectImportCode(code) importFilesCodes[path] = code }) } //完善import代码 function perfectImportCode(code){ let exportsCode = ` ( function(){ let require = function(path){ let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})() return exports } return (function(exports,require){${code} return exports })({},require) } )() ` return exportsCode } //开始解析import逻辑 perfectImport() //完善最终代码 function perfectCode(code){ let exportsCode = ` let require = function(path){ let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})() return exports } let exports = (function(exports,require){ ${code} return exports })({},require) console.log(exports.default) ` return exportsCode } //最后的代码 code = perfectCode(code) //删除文件操作 const deleteFile = (path)=>{ if(fs.existsSync(path)){ let files = [] files = fs.readdirSync(path) files.forEach((filePath)=>{ let currentPath = path + '/' + filePath if(fs.statSync(currentPath).isDirectory()){ deleteFile(currentPath) } else { fs.unlinkSync(currentPath) } }) fs.rmdirSync(path) } } deleteFile('cache') //写入文件操作 fs.mkdir('cache',(err)=>{ if(!err){ fs.writeFile('cache/main.js',code,(err)=>{ if(!err){ console.log('文件创建完成') } }) } })
古代钻木取火远比现代打火机烤面包的意义深远的多。这个世界做过的事情没有对或错之分,但有做与不做之别。代码拙劣,大神勿笑[抱拳][抱拳][抱拳]
更多node相关知识,请访问:nodejs 教程!
以上是淺析node怎樣連結多個JS模組的詳細內容。更多資訊請關注PHP中文網其他相關文章!