目录
前端工程户核心技术之模块化
前端模块化是一种标准,不是实现。commonjs是前端模块化的标准,而nodejs实现了一套commonjs的规范。
什么是前端模块化:将复杂程序根据规范拆分成若干个模块,每个模块间组织是有逻辑的,一个模块包括输入和输出,每个模块内部实现时私有的,对外暴露接口与其他模块通信,而不是直接调用的关系。这其实就是一种典型的面向对象编程思想。一个html页面可以引用的script标签包括:脚本和模块
脚本和模块的区别:
原来我们所有的业务逻辑代码只能写在index.js里,但用了模块化标准以后,就可以对他进行拆分了,可以把他拆分为一个入口文件entry.js,这个入口文件去引用若干个模块,然后把他们组织起来进行调用,浏览器去调用时,主要访问入口文件,再由入口文件调用其他模块化文件,组装成一个业务逻辑。脚本和模块化最典型的区别就是,通过模块化拆分后,我们可以更清楚的看到整个业务的逻辑,如果只有一个index.js你并不知道源码里面在干什么。
前端模块化的进化过程
定义的函数是挂载在window上的,如果有一个文件夹里也定义api函数,会引发全局命名空间冲突
commonjs规范介绍
commonjs是nodejs默认模块化规范,每个文件就是一个模块,有自己的作用域,可以维护自己的私有变量,node中cjs模块加载采用同步加载方式,必须等这个模块加载完了再去执行后面的代码,通过require加载模块,通过exports或者module.exports输出模块。
commonjs规范特点:
- 所有代码都运行在模块作用域,没有js脚本概念,写一个js文件就是一个模块,所以不会污染全局作用域。
- 模块可以多次加载,第一次加载时会运行模块,模块输出结果会被缓存,再次加载时,会从缓存结果中直接读取模块输出结果。
- 模块加载的顺序,按照其在代码中出现的顺序
- 模块输出的值是值的拷贝,类似iife方案中的内部变量
commonjs规范示例
每个功能创建一个文件,如api文件:
const handle = require('./handle')
function api(){
return {
code: 0,
data:{
a: 1,
b: 2,
}
}
}
module.exports = {
api,handle
}
handle.js文件:
function handle(data, key){
return data.data[key]
}
module.exports = handle;
sum.js文件:
function sum(a, b){
return a+b;
}
module.exports = sum;
然后在entry.js中引入各文件,执行相关业务逻辑:
首先,我们需要有一个主模块(即入口文件entry.js),在主模块中,我们会使用require加载模块,require加载模块时会把模块变成module对象,module对象中有一个load方法,通过load方法进行模块加载,在加载过程中,他会在模块的外层包一层,把原来的模块变成自适应函数,我们写的代码(比如写在api.js中的代码)变成函数里面的内容了,他会向自适应函数传入一些变量(require、module、exports、_filename、_dirname),这就是为什么在node里可以直接使用require的原因。使用module.exports输出模块时,最终输出结果会被缓存到module cache map中,这是一个键值对,键是module path+module name,值是module.exports。commonjs输出模块时,module.exports只能输出一个结果
commonjs模块打包
安装browserify:npm install browserify -g
打包命令:browserify module_test/cjs/entry.js -o dist/bundle.js
⚠️当存在多个entry.js模块时,每个entry.js模块都需要单独打包
browserify打包原理:
- 本质还是通过自执行函数实现模块化
- 将每个模块编号,存入一个对象,每个模块标记依赖模块
- 实现了require方法,核心是通过call方法调用模块,并传入require、module、exports方法,通过module存储模块信息,通过exports存储模块输出信息
amd规范、cmd规范
amd规范采用非同步(异步)加载模块,允许指定回调函数。node模块通常位于本地,加载速度快,所以适用于同步加载,但是在浏览器中,如果用同步加载会阻塞模块渲染(整个页面的渲染),所以在浏览器环境下,模块需要请求获取,适用于异步加载,所以诞生了amd规范,用来做异步加载,require.js是amd的一个具体实现库,amd、cmd目前很少使用,因为目前我们主要使用node和浏览器开发,分别使用commonjs和esmoudle。
cmd规范整合了commonjs和amd的优点,模块加载是异步的,cmd专门用于浏览器端,sea.js是cmd规范的一个实现,amd和cmd最大的问题是没有通过语法升级解决模块化,他们定义模块还是通过调用js方法来生成模块,这种方式利于我们快速应用,但是他没有办法对模块化进行规模化的应用,因为他们没有实现标准的语法规范
ESMoudle规范设计理念是希望在编译时就确定模块以来关系及输入输出,commonjs和amd必须在运行时才能确定依赖和输入、输出,ESMoudle通过import加载模块,通过exports输出模块
commonjs和ESMoudle规范的对比:
- commonjs模块输出的是值的拷贝,也就是说commonjs模块内部的值你想改是改不了的,但是es6模块输出的是值的引用
使用es6定义test.js文件:
export let a = 1;
export function plus() {
a++;
}
定义entry.js文件:
import {a, plus} from './test.js'
console.log(a);//1
plus();
console.log(a);//2
使用commonjs定义test.js:
let a = 1;
exports.a = a;
exports.plus = function(){
a++;
}
// 上述代码等同于:
// module.exports={
// a:1,// 这个a的值是直接拷贝过来的
// plus// plus里加的是模块里的a
// }
定义entry.js文件:
const { a, plus} = require(./test.js);
console.log(a);// 1
plus();
console.log(a);// 1
如果想要获取模块里的a,需要使用get方法,在commonjs定义的test.js中定义get方法:
let a = 1;
exports.a = a;
exports.plus = function(){
a++;
}
exports.get = function(){
return a;
}
在entry.js文件中调用get:
const { a, plus, get} = require(./test.js);
console.log(a);// 1
plus();
console.log(a);// 1
console.log(get());// 2
- commonjs模块是运行时加载,es6模块是编译时输出接口,在编译时就能确定导入哪些模块,输出哪些模块
- commonjs是单个值导出,es6可以导出多个
- commonjs模块为同步加载,es6支持异步加载,导出的是一个promise
- commonjs的this是当前模块的输出值,es6的this是undefined
- commonjs和es6的语法不同
script脚本和模块对比:模块具备更高的开发效率,可以将复杂的代码拆分成若干简单的代码,代码可读性强,复用高效,但是模块在加载过程中会损耗性能,因为模块文件很多,会使得加载速度变得更慢,而脚本具有更高的页面性能。模块在浏览器中运行会存在兼容性问题,要特别注意。所以在浏览器中运用模块化存在一些局限性,比如浏览器缺乏模块管理能力,模块分散在各个项目中,模块性能加载慢,无法在大型项目中直接使用,这两个问题是npm(解决模块管理能力问题)和webpack(解决模块加载慢问题)核心解决的问题
前端工程化关键技术之npm+webpack原理
npm实现的初步思路:
- 集中管理所有模块,所有模块都上传到仓库(registry)
- 模块内创建package.json标注模块的基本信息
- 通过nom publish发布模块,上传到仓库(registry)
- 通过nom install安装模块,模块安装到node_modules目录
npm解决的核心问题是模块管理问题,比如模块开发好了之后上传到哪里?如何进行快速复用?npm包含cli、模块仓库、官网三大部分。
首先,你产生了一个模块module1,需要通过npm init初始化或者说创建一个模块,常见的模块他只会包含package.json,在package.json里面我们通过修改name、version、dependencies确定模块的基本信息,如果这个模块想加载另外一个模块,那你需要通过npm install加载,加载完成后,另外一个模块会被下载到node_modules目录下,另外一个模块也是包含package.json和node_modules目录,这就是npm的规范。当module1开发完了之后,执行npm publish将其上传到npm仓库,仓库包含两部分,一个是公开部分,可以任意使用,但其中又分为普通仓库和组织的仓库;一个是private部分,也就是私有仓库。
npm原理总结:npm init创建模块,npm install安装模块,npm publish发布模块,npm link模块进行本地开发,npm config查看/调整本地配置,npm run调用package.json中的scripts。
npm规范:package.json管理模块信息,node_modules保存依赖
npm的局限:npm只能解决模块的高效管理和获取问题,无法解决页面性能加载问题,模块化发明后,制约其广泛应哟哦那个的因素就是性能问题
webpack实现原理
webpack主要是将所有的资源进行打包,打包到一个文件中。在原来的html模式中,如果你要去加载一些js,你需要通过script标签,如果你要去加载css,你需要编写css并做一个资源加载,这样在html中,就需要加载很多资源,如果通过webpack构建,首先会把js分为entry.js,然后在其中调用不同模块文件,这么多的资源如果按照原来的模式我们需要加载很多资源,但是webpack从entry.js开始,分析所有依赖并进行打包,然后将他们合成到一个bundle.js文件中,这样html在加载时就只用加载一个资源了,提高了页面性能
webpack的原理:最初的webpack核心解决的问题就是代码合并和拆分,webpack的核心里面是将资源都视为模块,统一进行打包和处理。webpack提供了loader和plugins完成功能扩展。
webpack
核心概念
entry:入口模块文件路径
output:输出bundle文件路径
module:模块,webpack构建对象
bundle:输出文件,webpack构建产物
chunk:中间文件,webpack构建的中间产物
loader:文件转换器,使用vue-loader工具让浏览器能运行vue文件,将vue文件输出为js文件
plugin:插件,执行特定任务,比如说将js文件依次分割成不同的文件,如png、css、js等
有一个js文件作为入口文件,这个js文件就是entry,他会引用不同的js模块、css模块,所有的这些都是从entry开始打包,然后依次去查找他们的依赖路径,将这个entry打包成bundle文件,在打包过程中会生成一些中间文件,这些中间文件就叫chunk
快速入门
终端执行:npm init
初始化项目
创建src/index.js文件
创建public/index.html
创建webpack.config.js并填入配置
const path = require('path')
module.exports = {
mode:'development',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname,'.dist'),// 这里必须使用path.resolve生成绝对路径,否则会报错。__dirname指向webpack.config.js所在完整路径,然后这个完整路径会拼上./dist
filename:'bundle.js'
}
}
执行npm install -D webpack -cli
配置build命令为webpack
执行npm run build
完成打包构建
通过查看打包后的bundle文件发现,bundle本质上是通过自适应函数实现模块化里面的私有化作用域
source-map原理讲解
在webpack.config.js中加入devtool选项,devtool的配置项可以从官网获得:
const path = require('path')
module.exports = {
mode:'development',
devtool:'source-map',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname,'.dist'),// 这里必须使用path.resolve生成绝对路径,否则会报错。__dirname指向webpack.config.js所在完整路径,然后这个完整路径会拼上./dist
filename:'bundle.js'
}
}
执行npm run build
打包后的文件跟不加devtool的有很大区别
sourceMappingURL=bundle.js.map这句话其实就是指定bundle.js对应的source-map文件的路径,即bundle.js.map。source-map维护了一个json文件,
file表示bundle.js.map对应的源码文件bundle.js;mapping表示我对bundle.js的哪一行代码进行映射,;表示换行,映射的内容为sourceContent的内容,AAAA是vlq码,可以看一下阮一峰老师的文章;source表示我们在浏览器中会生成一个新的资源文件资源文件内容就是sourceContent
通过webpack loader打包css文件
在index.js中引入css可以把css文件也打包到js中:import './index.css';
,这样在webpack打包时,可以对css进行模块化。安装css-loader:cnpm i -D css-loader
,还需要安装style-loader:cnpm i -D style-loader
,在webpack.config.js中添加配置:
const path = require('path')
module.exports = {
mode:'development',
devtool:'source-map',
entry:'./src/index.js',
output:{
path:path.resolve(__dirname,'.dist'),// 这里必须使用path.resolve生成绝对路径,否则会报错。__dirname指向webpack.config.js所在完整路径,然后这个完整路径会拼上./dist
filename:'bundle.js'
},
module:{
// module主要是针对模块化的一些内容
rules:[
// rules是个数组,配置的所有loader都在这里面
{
test:/.css$/,// 匹配所有以css结尾的文件
use:['style-loader','css-loader']// loader的执行顺序是从上到下,从右到左,所以一定是先执行css-loader,再执行style-loader
}]
}
}
执行npm run build
打包后,css被打包进了js中。css-loader会将css文件模块化,style-loader再将这个模块转化为dom,所以二者的执行顺序不能变。style-loader会在index.html中添加style标签,然后将css代码写入style标签中,通过这种方式将css和js结合