本文的初衷是来实现一个我们工作中最常用的构建工具,webpack,当然我们所实现的构建工具和真正的webpack差距甚远。这里只是简单一个实现罢了,感兴趣的同学可以继续看下去。
不过在说自动化构建之前还是要诉说一下模块化开发的发展史,这是前端er都经历过的一段历史,值得我们和所有人再去回顾一番!!!
模块化
模块化是指把一个复杂的系统分解到多个模块以方便编写
命名空间
开发网页要通过命名空间的方式来组织代码
<script src="jquery.js">
- 命名空间冲突,两个库可能会使用同一个名称
- 无法合理的管理项目的依赖和版本
- 无法方便的控制依赖的加载顺序
CommonJS
CommonJS是一种使用广泛的JavaScript模范化管理,核心思想是通过require方法来同步地加载依赖的其他模块,通过module.exports导出需要暴露的接口
用法
采用CommonJS导入及导出时的代码如下:
// 导入
const A = require('./a.js');
fn();
// 导出
module.exports = A.fn;
原理实现
// a.js
module.exports = '刚好遇见你';
//b.js
const fs = require('fs');
// CommonJS简单实现
function req(pathName) {
// content代表的是文件内容
let content = fs.readFileSync(pathName, 'utf8');
// 最后一个参数是函数的内容体
let fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content+'\n return module.exports');
let module = {
exports: {}
};
// 函数执行就可以取到module.exports的值了
return fn(module.exports, req, module, __filename, __dirname);
}
const str = req('./a.js'); // 导入a模块
console.log(str); // '刚好遇见你'
AMD
AMD也是一种JavaScript模块化规范,与CommonJS最大的不同在于它采用异步的方式去加载依赖的模块。 AMD规范主要是为了解决针对浏览器环境的模块化问题,最具代表性的实现是RequireJS
AMD的优点
- 可在不转换代码的情况下直接在浏览器里运行
- 可加载多个依赖
- 代码可运行在浏览器环境和Node环境中
AMD的缺点
- Js运行环境没有原生支持AMD,需要先导入实现了AMD的库才能正常使用(这里指的是RequireJS)
用法
// define定义模块
define('song', [], () => {
return '告白气球';
});
define('singer', ['song', 'album'], (song, album) => { // 依赖了song和album模块
let singer = '周杰伦';
return `${singer}的${song}属于专辑《${album}》`;
});
define('album', [], () => {
return '床边故事';
});
// require使用模块
require(['singer'], singer => {
console.log(singer); // 周杰伦的告白气球属于专辑床边故事
});
原理实现
RequireJS有两个方法,一个是define,另一个是require,所以首先我们先定义两个函数,看如下代码
let factories = {}; // 管理一个关联对象,将模块名和函数关联起来
// 定义模块define 三个参数:1.模块名 2.依赖 3.工厂函数
function define(name, depend, factory) {
factories[name] = factory;
factory.depend = depend; // 将依赖记到factory上
}
// 通过require使用模块
function require(modules, callback) {
let result = modules.map(mod => { // 返回一个结果数组
let factory = factories[mod]; // 拿到模块对应的函数
let exports;
let depend = factory.depend; // 取到函数上的依赖 ['a']
// require(['song','album'], function(song,album) {}) 可能会有很多依赖
require(depend, () => { // 递归require
exports = factory.apply(null, arguments);
});
return exports; // exports得到的是函数返回的值 如:'告白气球' , ' 床边故事'
});
callback.apply(null, result); // result为一个结果数组,所以用apply
}
★ define作用就是把定义模块的函数保留下来
★ require需要哪个模块的时候就把该函数执行,然后将执行后的结果传到回调里
ES6 模块化
- ES6模块化是ECMA提出的JS模块化规范,它在语言的层面上实现了模块化
- 最主要的是它将取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案
// 导入
import {each, ...} from 'underscore.js'; // es6 按需引入
var _ = require('underscore.js'); // amd 全局引入
// 导出
export {each, map, ...}; // es6 多点暴露
module.exports = _; // amd 全局暴露
小遗憾:ES6模块虽然是终极模块化方案,但它的缺点在于目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行
好了,以上内容就是模块化大致的发展历程了
如今我们在工作中开始大量使用ES6等先进的语法来开发项目了,但是正如ES6模块化的“小遗憾”一样,还有一些环境下并不能支持,所以本着为国为民的态度,我们还需将其转化为能够识别的代码
正因如此慢慢的出现了自动化构建,简单来说,就是把源码转换成发布到线上的可执行JS、CSS、HTML 代码,当然这是最主要的目的,除此之外还有很多用途(文件优化、自动刷新、模块合并、代码校验等),这里就不一一细说了。我们直接进入主题,来说说webpack这个目前构建工具中的爆款吧
webpack
webpack是一个打包模块化JS的工具,在webpack里一切文件皆模块,通过loader转换文件,通过plugin注入钩子,最后输出由多个模块组合成的文件。webpack 专注于构建模块化项目
安装webpack
安装到项目中
- 需要先在项目中npm init初始化一下
- 建议node版本安装到8.2以上
// 安装最新版
npm i webpack -D
// 安装指定版本
npm i webpack@<version> -D
// 目前webpack4已经发布 这里的安装需要多一个webpack-cli
npm i webpack webpack-cli -D
★ npm i -D 是 npm install --save-dev 的简写,是指安装模块并保存到 package.json 的 devDependencies
安装到全局
npm i webpack -g
注意:推荐安装到当前项目,原因是可防止不同项目依赖不同版本的webpack而导致冲突
使用webpack
默认情况下我们会将src下的入口文件进行打包
// node v8.2版本以后都会有一个npx
// npx会执行bin里的文件
npx webpack // 不设置mode的情况下 打包出来的文件自动压缩
// 设置mode为开发模式,打包后的文件不被压缩
npx webpack --mode development
这里使用webpack打包编译,是针对src目录下的默认文件index.js来做的处理
源文件目录结构
src -
- index.js
- a.js
// a.js
module.exports = '刚好遇见你';
// index.js
let str = require('./a.js');
console.log(str);
代码打包后会生成一个dist目录,并创建一个main.js,将打包后的代码放在其中,那么我们就来看看打包后的代码,到底是何方神圣
编译后的样子
打包后目录结构
dist -
- main.js
// 下面来看下内部,取其精华
(function (modules) {
function require(moduleId) { // moduleId代表的是文件名
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require("./src/index.js");
})
({
"./src/index.js": (function (module, exports, require) {
eval("let str = require(/*! ./a.js */ \"./src/a.js\");\n\nconsole.log(str);\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/a.js": (function (module, exports) {
eval("module.exports = '刚好遇见你';\n\n//# sourceURL=webpack:///./src/a.js?");
})
});
- 整体来说还是包在了一个自执行函数内,函数中的参数modules,其实就是下面()内的对象{}。
- modules[moduleId].call()这段代码其实就是将下面()内对应的key执行,modules['./src/index.js'] ()得到eval解析的代码
写一个试试
试试就试试,根据打包后的核心代码我们也来实现一个看看,来弄一个类似webpack脚手架,废话不多说,搞起来
// pack目录
pack -
- bin
- pack.js
- 首先我们先创建一个文件夹叫pack,里面有对应的文件
- 然后我们希望在命令行里直接执行pack命令就可以进行打包
- 必须是个模块才可以执行
- 在pack目录下npm init -y
- 初始化后的package.json文件中将bin下的pack路径改成"bin/pack.js"
- 将pack.js的命令引用到npm全局下,再执行pack命令的时候就可以直接使用
- 在pack目录下执行npm link就可将pack的包放到了npm全局下(mac下需要加sudo)
- 每次修改pack.js后,都需要重新npm link一下
- 命令行中再次执行pack便可以打包了
上面几项说的是一个整体流程,接下来我们开始实现pack.js里的主要逻辑
// pack.js
#! /usr/bin/env node
// 写上面这句话是告诉文件是在node下执行,不然会报错无法编译
let entry = './src/index.js'; // 入口文件
let output = './dist/main.js' // 出口文件
let fs = require('fs');
let path = require('path');
let script = fs.readFileSync(entry, 'utf8');
let results = [];
// 如果有require引用的依赖,那就需要替换处理依赖
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
let name = path.join('./src/', arguments[1]); // ./src/a.js
let content = fs.readFileSync(name, 'utf8');
results.push({
name,
content
});
return `require('${name}')`; // require('./src/a.js')
});
// 用ejs可以实现内容的替换
let ejs = require('ejs');
// 这里的模板其实就是dist/main.js里的核心代码
let template = `
(function (modules) {
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require("<%-entry%>");
})
({
"<%-entry%>": (function (module, exports, require) {
eval(\`<%-script%>\`);
})
<%for(let i=0;i<results.length;i++){
let mod = results[i];%>,
"<%-mod.name%>": (function (module, exports, require) {
eval(\`<%-mod.content%>\`);
})
<%}%>
});
`;
// result为替换后的结果,最终要写到output中
let result = ejs.render(template, {
entry,
script,
results
});
try {
fs.writeFileSync(output, result);
} catch(e) {
console.log('编译失败', e);
}
console.log('编译成功');
上面用到了ejs模板引擎,下面给大家写一下简单的用法
let name = '周杰伦';
console.log(<a><%-name%></a>); // <a>周杰伦</a>
实现一个loader
接下来再写一个loader吧,loader其实就是函数,我们加载个css样式进行编译看看。在src目录下添加一个style.css文件
// style.css
* {
margin: 0;
padding: 0;
}
body {
background: #0cc;
}
// index.js引入css文件
let str = require('./a.js');
require('./style.css');
console.log(str);
根据代码添加一个style-loader去编译css文件
// pack.js
// 省略...
let results = [];
// loader其实就是函数
// 这里写一个style-loader
+ let styleLoader = function(src) {
// src就是样式中的内容
return `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(src).replace(/(\\r)?\\n/g, '')};
document.head.appendChild(style);
`;
+ };
// 如果有require引用的依赖,那就需要替换处理依赖
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
let name = path.join('src', arguments[1]); // ./src/a.js
let content = fs.readFileSync(name, 'utf8');
// 如果有css文件,就进行编译
+ if (/\.css$/.test(name)) {
+ content = styleLoader(content);
+ }
results.push({
name,
content
});
return `require('${name}')`; // require('./src/a.js')
});
这里用JSON.stringify处理字符串不能换行的问题,如下代码
body {
background: #0cc;
}
但是有个小瑕疵就是会带上换行符,所以replace这里来处理stringify后的\r\n换行符(mac下只有\n)
写到这里,一个简单的编译工具就完成了,这个功能虽然很不完善,但是也是开拓一下视野去实现一下我们常用的webpack是如何从0到1的过程。
当然我们实现的这个不能和成熟的webpack去比较,而且webpack的实现不仅于此。我也在尝试着继续研究webpack如何实现,像涉及到的ast这些内容,也在慢慢学习中。
希望下一次再写的时候会给各位观众一个更高大上的webpack实现了!
感谢各位的观看了。此致,敬礼!