(1995)Brendan Eich发明Javascript -> (2005)Ajax广泛应用 -> (2008)V8引擎发布 ->(2009)Node.js发布 ->(2010)NPM 0.1版发布 -> (2013) Webpack 1.0版发布 -> (2013)React 1.0版发布 -> (2014) Vue 1.0版本发布
开发方式的演进
服务器端渲染
浏览器 ->(提交Form)–(服务器) -> (服务端语言JAVA、PHP、C#、Vb.net)<–>数据库/Service/–>(模板、JSP、CSS+JS)–(HTML) ->浏览器
前后端分离
组件化
主流前端技术栈
框架:Vue、React、Angular
语言:TS、ES6
工程化工具:Webpack、Vite、Rollup、Gulp
跨平台技术:Electron、React Native、Flutter、小程序
??前端工程化解决了哪些痛点?
项目的核心三要素:时间、质量、成本
脚手架: react(create-react-app)、vue(vue-cli);
创建项目:开箱即用;热更新:所见即所得,修改马上生效;
JS的兼容:ES6、TS --> babel-loader -> ES5 – sass scss less -> css -webkit- -moz-
每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。
加载某个模块,其实是加载该模块的module.exports属性。require方法用于加载模块。
加载方式:同步加载
所有代码都运行在模块作用域,不会污染全局作用域;
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。再想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序;
模块输出的值是值的拷贝:从而控制了数据的访问权限;
require命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load;
Module._load = function(request,parent,isMain){ //1.检查Module._cache,是否缓存之中有指定模块 //2.如果缓存之中没有,就创建一个新的Module实例 //3.将它保存到缓存; //4.使用module.load()加载指定的模块文件,读取文件内容后,使用module.compile()执行文件代码 //5.如果加载/解析过程报错,就从缓存删除该模块 //6.返回该模块的module.exports };
一旦require函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样就可以避免污染全局环境。该函数的参数包括require、module、exports,以及其他一些参数。
(function(exports,require,module,__filename,__dirname){ //你的代码被导入在这里 });
不能在浏览器中直接使用,而需要借用以下工具;
npm install browserify -g
browserify inputPath.js -o outputPath.js —>browserify index.js->(输入文件名) -o bundle.js->(输出文件名) \eg: browserify index.js -o bundle.js
官网:https://browserify.org/
AMD全称是Asynchronous Modules Definition异步模块定义,提供定义模块及异步加载该模块依赖的机制,这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。
AMD规范只定义了一个函数“define”,它是全局变量。模块通过define函数定义在闭包中,格式如下:
define(id?String,dependencies?:String[],factory:Function|Object);
代表:require.js
Common Module Definition, 通用模块定义。
异步加载,可以像在Node环境中一样来书写模块代码。代码的书写格式如下:
define(function(require,exports,module){
var $ = require('jquery');
exports.sayHello = function(){
$('#hello').toggle('slow');
};
});
代表:sea.js
在编译阶段确定依赖关系和输入输出。
export导出模块:export为普通导出、export default为默认导出;
import加载模块
特点:
- CommonJS
- 导出值是值的拷贝
- 单个值导出
- 运行时加载
- 同步加载
- 模块中this指向当前模块- ESModule
- 导出值是值的引用
- 多个值导出
- 编译时加载
- 支持同步和异步加载
- 指向undefined
NPM的全称时Node Package Manager, 是一个NodeJS包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准。
官网:https://www.npmjs.com/
特点:
所有模块都在仓库中集中管理,统一分发使用
在package.json文件中记录模块信息,依赖关系等
通过publish命令发布到仓库
通过install进行按照
npm init: 创建项目 npm init -y
npm install: 安装模块 npm install -S/生产环境和开发环境中都可用的包 -D/只开发环境中使用的包
npm publish: 上传到仓库(public/private)
npm run xxx: 执行指定的脚本
npm info: 查看包信息
npm ls xxx: 查看安装的包的版本号
npm config: 配置信息 npm config -help
npm config get registry: 查看当前使用的镜像
npm config set registry “https://r.cnpmjs.com”: 修改镜像源
新建.npmrc文件: 可以在项目里配置该项目适用的特殊项,eg: registry=“https://www.baidu.com”
- 发展历史:
- 2012年3月10号诞生
- 作者德国人Tobias(Java工程师),从事将Java转为js的研究(GWT)-(谷歌web工具库),里面有个特性叫“code splitting”
- "code splitting"就是Webpack现在提供的主要功能
Webpack 是一个前端资源加载、打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。
Webpack 可以将多种静态资源js、css、less转换成一个静态文件,从而减少了页面的请求。
webpack.config.js是webpack的配置文件
module.exports = {
mode: "development",//"development"|"production"|"none"
entry: "./app.js",
output: {
path: __dirname,
filename: "./bundle2.js",
}
}
- App瘦身 400MB -> 200MB
- eg:
js -> rich editor -> template 40MB 修改库:修改代码中的引用 fork -> 修改源代码 —> || repo -> 修改源代码 -> 1. npm install git+// 2. npm publish @test/richEditor npm install @test/richEditor
license 协议:MIT/- 使用COS(存储桶)实现CDN加速(devOps VS 前端工程化)
- jenkins(触发器)/ -> gitlab && github
- rollup
在web浏览器中运行Javascript有两种方法。第一种方式,引用一些脚本来存放每个功能;此方案特别难扩展,因为加载太多脚本会导致网络瓶颈。第二种方式,使用一个包含所有代码的大型.js文件,但是这个方法会导致作用域,文件大小,可读性,可维护性等方面的问题出现。
- 依赖自动收集
传统的任务构建工具基于Google的Closure编译器,都要求用户手动在顶部声明所有的依赖。然而像webpack一类的打包工具自动构建并基于用户所引导或导出的内容推断出依赖的图谱。这个特性与其他的如:plugin and loader一道让开发者用户体验更好。
- 前端模块化开发支持(解决作用域污染等问题,提高代码可维护性)
- 代码压缩混淆(减少传输时间,加快页面打开速度,保护源代码)
- 处理浏览器JS兼容问题(ES5、ES6等多版本兼容)
- 性能优化(SEO)
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
```
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist'),
}
}
npx webpack --config webpack.config.js
- HotModuleReplacementPlugin -D (开发时使用的模块) -> 模块热更新
- clean-webpack-plugin -> 目录清理
- html-webpack-plugin -> 自动生成一个index.html文件,将打包的js文件自动通过script标签
- uglifyjs-webpack-plugin -> js压缩
- mini-css-extract-plugin -> 分离样式文件,CSS提取为独立的文件
- webpack-bundle-analyzer -> 可视化Webpack输出文件的体积(业务组件、依赖第三方模块)
- DefinePlugin -> 定义全局常量,应用:为开发和生产环境引入不用的配置;eg如下:
js plugins:[new webpack.DefinePlugin({ HOST:JSON.stringify("https://api.dev.com"), })]
- style-loader -> 用于将css文件注入到index.html中的style标签上,通常与css-loader一起使用
- css-loader -> 用于处理css文件,使得在js文件中引入使用,通常与style-loader一起使用
- less-loader -> 处理less代码
- sass-loader -> 处理sass代码
- babel-loader -> 把ES6转为ES5
- ts-loader -> 把Typescript转换成ES5
- file-loader -> 打包图片,打包字体图标
- url-loader -> 和file-loader类似,但是当文件小于设定的limit时,可以返回一个DataUrl(提升网页性能)
- html-withimg-loader -> 打包HTML文件中的图片
- eslint-loader -> 用于检查常见的JavaScript代码错误,也可以进行“代码规范”检查
没有优化的原始配置
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-entryModule.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname,'dist'),
},
};
防止重复引入(Prevent Duplication)
- 1.使用shared属性
js const path = require('path'); module.exports = { mode: 'developoment', entry: { index: { import: './src/index.js', dependOn: 'shared', }, another: { import: './src/another-entryModule.js', dependOn: 'shared', }, shared: 'lodash', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname,'dist'), }, }
- 2.使用SplitChunksPlugin
js const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-entryModule.js', }, output:{ filename: '[name].bundle.js', path: path.resolve(__dirname,'dist'), }, optimization: { splitChunks: { chunks: 'all', }, }, };
- 3.动态导入(Dynamic Imports)
使用异步方式导入模块
js //优化前 import _ from 'lodash'; //优化后 //在需要使用的地方去import const { default: _ } = await import('lodash');
- 4.Caching缓存
contenthash
文件名可以添加属性[contenthash],当文件内容改变时会跟着一同改变,防止文件被缓存。
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management',
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname,'dist'),
clean: true,
},
};
- 5.提取文件范例(Extracting Boilerplate)
webpack 提供了一个优化功能,可使用optimization.runtimeChunk选项将runtime代码拆分为一个单独的chunk。将其设置为single来为所有chunk创建一个runtime bundle:
module.exports = {
entry: './scr/index.js',
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({title:'Caching'}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname,'dist'),
},
optimization: {
runtimeChunk: 'single'
}
}
模块定义,键值对
key是文件路径
value是通过闭包加载js文件中的内容
模块的缓存,确保模块只加载一次
相当于CommonJS规范中的require函数
在exports中定义_esModule属性,值为true
在exports上挂载要导出的属性和方法
判断obj对象是否包含指定的属性attr
什么是ast
ast:抽象语法树(abstract syntax tree),是源代码的抽象语法结构的树状表现形式。树上的每个节点都表示源代码中的一个结构。
之所以说语法是【抽象】的,是因为这里的语法并不会表示出真实语法中出现的每个细节。使用场景:
- 语法解析,检查
- 代码格式化,高亮,错误提示,代码自动补全
- Babel编译ES6语法
- 代码压缩,混淆代码
- 可以编写有独特语法特征的高级框架,例如react,vue等
ast的转换工作流程
源代码 --> 原语法树 --> 遍历语法树上的各个节点 --> 对语法树进行修改转换 --> 新的语法树 --> 根据新的语法树重新生成代码
- 分词(tokenize)
将一行行的源码拆解成一个个token。所谓token,是指语法上不可能再分,最小的单个字符或字符串。
6 * 7 的语法树
var userName = “油茶”
var(关键字keyword) useName(标识符identifier) =(赋值assignment) “油茶”(字符串Literal)
- 实际使用:将function的驼峰命名转换为下划线命名
需要使用到的库
驼峰命名转换为下划线的方法:
str.replace(/[A-Z]/g,(a)=>`_${a.toLowerCase()}`);
源代码
function weWantMoney(){}
实战:
npm init -y
// 这里我用的是yarn安装
yarn add esprima estraverse escodegen -D
// 创建输入文件input.js
function weWantMoney(){
console.log('$$$');
}
// 创建index.js文件
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
const fs = require('fs');
const inputSoure = fs.readFileSync('./input.js').toString();
// 把代码转换成ast树形结构
const astTree = esprima.parseModule(inputSoure);
console.log(astTree);
// 遍历树上的所有结点
estraverse.traverse(astTree,{
enter: (node) => {
console.log('----node.type',node.type);
if(node.type === 'FunctionDeclaration') {
node.id.name = node.id.name.replace(/[A-Z]/g,(a)=>`_${a.toLowerCase()}`)
}
},
leave: (node) => {
console.log('---leave');
}
})
//把ast树形结构,转换为代码
const newCode = escodegen.generate(astTree);
console.log('---result',newCode)
fs.writeFileSync('./output.js',newCode)
- webpack是使用acornjs来解析把js解析成ast tree的。
acornjs 3792行- webpack是如何解析css语法树的:
CssParser.js 119行parse方法阅读几个核心function
- eatWhiteLine: 删除空白行
- eatUntil: 删除符合条件之前的内容
- eatText: 删除注释并返回新字符串
如何提取css中的url属性
walkCssTokens.js 592行
// 开始每个字符串进行检查
CssParser.walkCssTokens
// 当碰到字母u的时候 288行
CHAR_MAP[cc](input,pos,callbacks)
···
case CC_LOWER_U:
return consumePotentialUrl;
···
// 从pos位置开始进行字符串提取
consumePotentialUrl
// 其余完成之后交给回调函数来处理
CssParser_url
loader 是一个函数,通过它我们可以在webpack处理特定资源(文件)之前进行预处理。
例如: webpack仅仅只能识别javascript模块,而我们在使用TypeScript编写代码时可以提取通过babel-loader将.ts后缀文件提取编译称为JavaScript代码,之后再交给Webpack处理。
- 基础配置示例
module.exports = {
module: {
rules: [
{ test: /.css$/, use: 'css-loader', enforce: 'post' },
{ test: /.ts$/, use: 'ts-loader' },
],
},
};
- 参数
- test
test是一个正则表达式,根据test的规则去匹配文件。如果匹配到,那么该文件就会交给对应的loader去处理。
```js
module.exports = {
module: {
rules: [
{ test: /.css$/, use: 'sass-loader', enforce: 'pre' },
{ test: /.css$/, use: 'css-loader' },
{ test: /.css$/, use: 'style-loader', enforce: 'post' },
],
},
};
```
(《pre-loader》 --> 《normal-loader》 --> 《inline-loader》 --> 《post-loader》)
(《post-loader》 <-- 《inline-loader》 <-- 《normal-loader》 <-- 《pre-loader》)
( (loader) <-- (loader) <-- (loader) <-- (loader) ) <-- read resource
(《pre-loader》 <-- 《normal-loader》 <-- 《inline-loader》 <-- 《post-loader》)
read resource <-- pitch <-- pitch <-- pitch <-- pitch
- 使用!前缀,将禁用所有已配置的 normal loader
import Styles from '!style-loader!css-loader?modules!./styles.css';
- 使用!!前缀,将禁用所有已配置的loader (pre loader,normal loader,post loader)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
- 使用-!前缀,将禁用所有已配置的 pre loader 和 loader,但是不禁用 post loader
import Styles from '-!style-loader!css-loader?modules!./styles.css';
loader的执行阶段分为两个阶段:
Pitch 阶段:loader上的pitch方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。
Normal 阶段:loader上的常规方法,按照前置 (pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。
function loader(){
// 正常的loader执行阶段...
}
// remainingRequest:表示剩余需要处理的loader的绝对路径,如果多个路径以!为分割组成的字符串
// precedingRequest: 表示pitch阶段已经迭代过的loader,如果多个路径按照!分割组成的字符串
// data: 在pitch和loader之间交互数据
loader.pitch = function (remainingRequest, precedingRequest, data) {
// pitch loader
// 如果return了非undefined的值,会带来熔断效果
}
如果loader.pitch函数中return了非undefined的值,会带来熔断效果。如下效果:
loader -1 | loader -2 | loader -3 | loader -4 | loader -5
loader <— loader —-— loader —x— loader —x— loader
pitch —> pitch —> pitch(发生熔断) —> loader(回传到 loader -2 的loader),熔断后loader-4/5都不会执行
loader api org详解
module.exports = function(content,map,meta){
return someSyncOperation(content);
}
function asyncLoader() {
return Promise((resolve) => {
// do something
// resolve的值相当于同步loader的返回值
resolve('this is the result value')
})
}
function asyncLoader() {
const callback = this.async()
// do something
// 结束运行并把值返回
callback('this is the result value')
}
为了更好的理解pitch的作用,可以看一下style-loader的代码。
style-loader org
style-loader
loader.pitch生成的代码:
module.exports.pitch = function (request) {
return `
import API from"!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
import domAPI from"!../node_modules/style-loader/dist/runtime/styleDomAPI.js";
import insertFn from"!../node_modules/style-loader/dist/runtime/insertBySelector.js";
import setAttributes from"!../node_modules/style-loader/dist/runtime/setAttributesWithAttributes.js";
import insertStyleElement from"!../node_modules/style-loader/dist/runtime/insertStyleElement.js";
import styleTagTransformFn from"!../node_modules/style-loader/dist/runtime/styleTagTransform.js";
import content, * as namedExport from"!!../node_modules/css-loader/dist/cjs.js!./style.css";
var options = {};
options.styleTagTransform = styleTagTransformFn;
options.setAttributes = setAttributes;
options.insert = insertFn.bind(null,"head");
options.domAPI = domAPI;
options.insertStyleElement = insertStyleElement;
var update = API(content,options);
`;
}