function ajax(url, method) {
return new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.responseType = "json";
xhr.onload = function() {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
xhr.send();
ajax("api/test.json", "get").then(function(value) {
console.log(value);
}, function(error) {
console.log(error);
Promise最常见的错误是嵌套使用(回调地域)。
Promise链式调用:Promise对象的then()方法会返回一个全新的Promise对象,后面的then方法就是在为上一个then返回的Promise注册回调,前面then方法中回调函数的返回值会作为后面then方法返回回调的参数,如果回调中返回的是Promise,那后面then方法的回调会等待它的结束。
Promise静态方法(直接返回一个Promise对象): Promise.resolve().then()
/ Promise.reject().catch()
Promise.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透,如下题:
Promise.all()内部的全部异步执行完毕后then,Promise.race()内部的异步谁先执行完就then或者catch返回谁(用于实现处理请求超时)。
微任务会在当前线程执行完后立即执行,而宏任务要等线程和微任务都结束后才执行,目前绝大多数异步调用是宏任务,Promise是微任务。
Generator生成器异步方案:
// 执行生成器的公用函数
function co(generator) {
// 迭代生成器方法
function handleResult(result) {
if (result.done) {
return;// 生成器函数结束
result.value.then(data => {
handleResult(g.next(data));
}, error => {
g.throw(error);
const g = generator();
handleResult(g.next());
// 自定义生成器
function * main() {
const data1 = yield ajax("/api/test.json", "get");
console.log(data1);
const data3 = yield ajax("/api/error.json", "get");
console.log(data3);
const data2 = yield ajax("/api/info.json", "get");
console.log(data2);
} catch(e) {
console.log("报错信息:", e);
// 执行生成器方法,内部的异步请求将按顺序依次往下执行
co(main);
任务三:TypeScript语言
1. 强类型语言的优势(弱类型的缺点):a.错误更早暴露;b.代码更智能;c.重构更牢靠;d.减少不必要的类型判断。
2. Flow:javacript的类型检查器。Flow安装:yarn add flow-bin --dev; 生成Flow配置文件:yarn flow init; 启动/停止Flow服务:yarn flow / yarn flow stop; Flow编译移除注解:a.flow-remove-types;b.babel/perset-flow。
3. ts中文错误提示: yarn tsc --locale zh-CN。
4. 解决作用域问题:a.放到一个立即执行函数中;b.末尾添加 export {}。
5. typescript命令:a.安装:yarn add typescript;b.配置文件:yarn tsc --init;c.编译:yarn tsc;d.调试:yarn add nodemon/yarn add ts-node/yarn nodemon xxx.ts
6. typescript数组类型例子:
function sum(...args: number[]) {
return args.reduce((prev, current) => prev + current, 0);
sum(1, 2, 3);
typescript类型总结(示例):
// 【原始数据类型】
const a: string = 'foobar'
const b: number = 100 // NaN Infinity
const c: boolean = true // false
// 在非严格模式(strictNullChecks)下,
// string, number, boolean 都可以为空
// const d: string = null
// const d: number = null
// const d: boolean = null
const e: void = undefined
const f: null = null
const g: undefined = undefined
// Symbol 是 ES2015 标准中定义的成员,
// 使用它的前提是必须确保有对应的 ES2015 标准库引用
// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
const h: symbol = Symbol()
// -------------------------------------------------------------------------
// 【Object 类型】
// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {}
// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
// -------------------------------------------------------------------------
// 【数组类型】
const arr1: Array<number> = [1, 2, 3]
const arr2: number[] = [1, 2, 3]
// -------------------------------------------------------------------------
// 【元组类型】
// 元组类型是指明确每一个元素类型和元素数量的数组,Object.entries()返回的就是一个元组类型
const tuple: [number, string] = [18, 'zce']
const entries: [string, number][] = Object.entries({
foo: 123,
bar: 456
const [key, value] = entries[0];
// 打印entries: [["foo", 123], ["bar", 456]]
// -------------------------------------------------------------------------
// 【枚举类型】
enum PostStatus { // 这种枚举会造成编译时的代码入侵
Draft = 1,
Unpublished = 2,
Published = 3
const enum PostStatus { // 常量枚举,不会侵入编译结果
Draft = 1,
Unpublished,
Published
// -------------------------------------------------------------------------
// 【函数类型】
function func1 (a: number, b: number = 10, ...rest: number[]): string {
return 'func1'
func1(100, 200, 300, 400)
const func2: (a: number, b: number) => string = function (a: number, b: number): string {
return 'func2'
// -------------------------------------------------------------------------
// 【接口】
interface Post {
title: string
content: string
function printPost (post: Post) {
console.log(post.title)
console.log(post.content)
printPost({
title: 'Hello TypeScript',
content: 'A javascript superset'
类型断言:类型断言不是类型转换,它是用来明确某一个变量的具体类型,其方式有两种:
const num = res as number;
const num2 = <number>res // JSX下不能使用
interface接口的作用在于约束一个对象的结构,一个对象要实现一个接口,就必须拥有接口中约束的所有成员。
interface Post {
title: string
subtitle?: string // 可选成员
readonly summary: string // 只读成员
const hello: Post = {
title: 'Hello TypeScript',
summary: 'A javascript'
interface Cache {
[prop: string]: string //动态成员
const cache: Cache = {}
cache.foo = 'value1'
ts中的类对es6的类进行了语法上的增强,跟java很像,如下:
class Person {
public name: string // = 'init name'
private age: number // private表示外部不可访问
// protected表示外部不可以但是子类可以访问;readonly表示不可修改且赋值只能在初始化或构造器
protected readonly gender: boolean
constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
console.log(this.age)
class Student extends Person {
private constructor (name: string, age: number) { // 构造器私有表示不能外部实例化
super(name, age)
console.log(this.gender)
static create (name: string, age: number) { // 使用静态方法创造实例
return new Student(name, age)
const jack = Student.create('jack', 18)
类的接口与实现:
interface Eat {
eat (food: string): void
class Person implements Eat{
eat (food: string): void {
console.log(`优雅的进餐: ${food}`)
抽象类/方法:class/方法名前加abstract:
abstract class Animal {
eat (food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
abstract run (distance: number): void
抽象类和接口的区别:在typescript中接口和抽象类有什么区别。
// function createNumberArray (length: number, value: number): number[] {
// const arr = Array<number>(length).fill(value)
// return arr
// function createStringArray (length: number, value: string): string[] {
// const arr = Array<string>(length).fill(value)
// return arr
function createArray<T> (length: number, value: T): T[] {
const arr = Array<T>(length).fill(value)
return arr
Lodash是一个著名的javascript原生库,不需要引入其他第三方依赖。是一个意在提高开发者效率,提高JS原生方法性能的JS库(另有query-string,这些之后要去项目中学习使用起来)。
typescript中引入第三方模块的时候,可以:a.自己手动用declare声明模块;b.用yarn add @types/模块名 添加声明;c.第三方库有可能已经自己集成兼容ts了。
模块二:函数式编程与 JavaScript 性能优化
任务一:函数式编程范式
函数是一等公民是指,函数可以作为参数、可以作为返回值、可以赋值给变量。
高阶函数--函数作为参数(回调):
// array的forEach()、filter()、some()、map()、every()等方法,都是高阶函数
// 模拟foreach
function forEach(arr, fn) {
for (let i = 0; i < arr.length; i++) {
fn(arr[i]);
let arr = [2,3,5,8,12];
forEach(arr, item => console.log(item));
// 模拟filter
function filter(arr, fn) {
let results = [];
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
results.push(arr[i]);
return results;
let arr2 = [2,3,5,8,12];
console.log(filter(arr2, item => item % 2 === 0));
return function() {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(this, arguments)
return cache[key]
function getCircleArea(r) {
console.log("first exct");
return Math.PI * r * r
let getAreaWithMemory = memoize(getCircleArea);
console.log(getAreaWithMemory(4)); // 会打印"first exct"
console.log(getAreaWithMemory(4)); // 已有缓存,不会重复执行
if (args.length < fn.length) {
// return function () {
// return curriedFn(...args.concat(Array.from(arguments)))
return function (...rest) {
return curriedFn(...args.concat(rest))
return fn(...args)
// es6写法
const curry = fn => curriedFn = (...args) => args.length < fn.length ? (...rest) => curriedFn(...args.concat(rest)) : fn(...args)
const getSum = (a, b, c) => a + b + c;
const getSumCurried = curry(getSum);
console.log(getSumCurried(1)(2)(3));
function compose(...args) {
return function (value) {
return args.reverse().reduce(function(acc, fn) {
return fn(acc)
}, value)
// es6写法
// const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value);
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const fn = compose(first, reverse);
console.log(fn(["jack", "tom", "rose"]));
PointFree:只需要定义一些辅助的基本运算函数,然后合成运算过程,不需要指明处理的数据,pointfree案例如下:
// 把一个字符串中的首字母提取并转换成大写, 使用. 作为分隔符
// world wild web ==> W. W. W
const fp = require('lodash/fp')
// const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'))
函子(Functor):是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)。可以用来在函数式编程中把副作用控制在可控的范围内、异常处理、异步操作等。
基本函子:
// Pointed函子:实现了of静态方法的函子
class Container {
static of (value) {
return new Container(value)
constructor (value) {
this._value = value
map (fn) {
return Container.of(fn(this._value))
// let r = Container.of(5)
// .map(x => x + 2)
// .map(x => x * x)
// console.log(r)
IO函子,中的_value是一个函数,这里是把函数作为值来处理,可以把不纯的动作存储到 _value中,用来延迟这个不纯的操作,把不纯的操作交给调用者来处理:
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO(function () {
return value
constructor (fn) {
this._value = fn
map (fn) {
return new IO(fp.flowRight(fn, this._value))
let r = IO.of(process).map(p => p.execPath)
console.log(r._value())
Folktale:一个标准的函数式编程库,和loadsh、remda不同,没有提供很多功能函数,而是只提供了一些函数式处理(如compose、curry)和一些函子(Task、Either、MayBe等)。
Task函子(处理异步任务):
// Task 处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile (filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
onResolved: value => {
console.log(value)
Monad函子:可以变扁的Pointed函子,具有join()和of()两个方法,可以用来解决IO函子嵌套过多的问题。
// IO Monad
const fs = require('fs')
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO(function () {
return value
constructor (fn) {
this._value = fn
map (fn) {
return new IO(fp.flowRight(fn, this._value))
join () {
return this._value()
flatMap (fn) {
return this.map(fn).join()
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
let print = function (x) {
return new IO(function () {
console.log(x)
return x
let r = readFile('package.json')
// .map(x => x.toUpperCase())
.map(fp.toUpper)
.flatMap(print)
.join()
标记清除原理:遍历并标记活动对象,遍历并清除没有标记的对象。优点:可以解决循环引用不能回收的问题。缺点:回收后的空间是碎片化的,不能使空间得到最大化的使用;不会立即回收垃圾对象。
标记整理原理:标记清除的增强,标记阶段一致,清除阶段会先执行整理,移动对象位置。优点:可以解决空间碎片化问题。缺点:不会立即回收垃圾对象。
V8是一个js引擎,特点是采用即时编译和内存设限。
V8垃圾回收策略:分代回收、空间复制(新生代)、标记整理(新、老生代)、标记清除(老生代)、标记增量(老生代)。
界定内存问题的标准:a.内存泄漏:内存使用持续升高;b.内存膨胀:在多数设备上都存在性能问题;c.频繁的垃圾回收:通过内存变化图进行分析。
监控内存的几种方式:a.浏览器任务管理器;b.timeline时序图记录;c.堆快照查找分离dom;d.判断是否存在频繁的垃圾回收。
基于Benchmark.js的Jsperf可以进行js性能的单元测试。
js性能优化策略:
// 【1.慎用全局变量】
function fn() {
// name = 'lg'
const name = 'lg'
console.log(`${name} is a coder`)
// 【2.缓存全局变量】
function getBtn2() {
let obj = document
let oBtn1 = obj.getElementById('btn1')
// 【3.通过原型对象添加附加方法】
var fn1 = function() {
// this.foo = function() {
// console.log(11111)
fn1.prototype.foo = function() {
console.log(11111)
// 【4.避开闭包陷阱】
// 【5.避免属性访问方法使用】
function Person() {
this.name = 'icoder'
this.age = 18
// this.getAge = function() {
// return this.age
const p2 = new Person()
// const a = p1.getAge()
const a = p2.age
// 【6.for循环优化】
for (var i = 0,len = arrList.length; i < len; i++) {
console.log(arrList[i])
// 【7.选择最优循环方法】
// foreach > for > forin
// 【8.文档碎片优化节点添加】
var oP = document.createElement('p')
// document.body.appendChild(oP)
const fragEle = document.createDocumentFragment()
fragEle.appendChild(oP)
document.body.appendChild(fragEle)
// 【9.克隆优化节点操作】
var oldP = document.getElementById('box1')
for (var i = 0; i < 3; i++) {
// var newP = document.createElement('p')
var newP = oldP.cloneNode(false)
newP.innerHTML = i
document.body.appendChild(newP)
// 【10.直接量替换new object()】
前端工程化主要解决的问题:
(1) 传统语言或语法的弊端;(2) 无法使用模块化/组件化;(3) 重复的机械式工作;
(4) 代码风格统一、质量保证;(5) 依赖后端服务接口支持;(6) 整体依赖后端项目;
工程化表现:一切以提高效率、降低成本、质量保证为目的的手段都属于工程化;
任务二:脚手架工具
node的环境变量配置:
在node安装路径下新建node_global、node_cache文件夹;
在终端运行:
npm config set prefix "D:\Node\nodejs\node_global"
npm config set cache "D:\Node\nodejs\node_cache"
(1) package.json中定义:"bin":"test-cli.js"
(2) 根目录建test-cli.js文件和templates模板文件夹
(3) 编写test-cli.js:
#!/usr/bin/env node
// Node CLI 应用入口文件必须要有这样的文件头
// 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改
// 脚手架的工作过程:
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs')
inquirer.prompt([
type: 'input',
name: 'name',
message: 'Project name?'
.then(anwsers => {
// console.log(anwsers)
// 根据用户回答的结果生成文件
// 模板目录
const tmplDir = path.join(__dirname, 'templates')
// 目标目录
const destDir = process.cwd()
// 将模板下的文件全部转换到目标目录
fs.readdir(tmplDir, (err, files) => {
if (err) throw err
files.forEach(file => {
// 通过模板引擎渲染文件
ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => {
if (err) throw err
// 将结果写入目标文件路径
fs.writeFileSync(path.join(destDir, file), result)
"scripts": {
"build": "sass scss/main.scss css/style.css --watch",
"serve": "browser-sync . --files \"css/*.css\"",
"start": "run-p build serve"
yarn start
常用的自动化构建工具:Grunt(生态完善,但基于临时文件、构建速度慢)、Gulp(基于内存、构建速度快、效率高、更方便)、FIS(百度推出,集成比较多,大而全)。
Grunt的使用:
yarn init --yes
yarn add grunt
code gruntfile.js
// gruntfile.js
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 的对象类型的形参
// grunt 对象中提供一些创建任务时会用到的 API
module.exports = grunt => {
grunt.registerTask('foo', 'a sample task', () => {
console.log('hello grunt')
grunt.registerTask('bar', () => {
console.log('other task')
// // default 是默认任务名称
// // 通过 grunt 执行时可以省略
// grunt.registerTask('default', () => {
// console.log('default task')
// })
// 第二个参数可以指定此任务的映射任务,
// 这样执行 default 就相当于执行对应的任务
// 这里映射的任务会按顺序依次执行,不会同步执行
grunt.registerTask('default', ['foo', 'bar'])
// 也可以在任务函数中执行其他任务
grunt.registerTask('run-other', () => {
// foo 和 bar 会在当前任务执行完成过后自动依次执行
grunt.task.run('foo', 'bar')
console.log('current task runing~')
// 默认 grunt 采用同步模式编码
// 如果需要异步可以使用 this.async() 方法创建回调函数
// grunt.registerTask('async-task', () => {
// setTimeout(() => {
// console.log('async task working~')
// }, 1000)
// })
// 由于函数体中需要使用 this,所以这里不能使用箭头函数
grunt.registerTask('async-task', function () {
const done = this.async()
setTimeout(() => {
console.log('async task working~')
done()
}, 1000)
yarn grunt async-task
grunt.registerMultiTask('build', function () {
console.log(`task: build, target: ${this.target}, data: ${this.data}`)
Grunt常用插件使用:sass、babel、watch:
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
grunt.initConfig({
sass: {
options: {
sourceMap: true,
implementation: sass
main: {
files: {
'dist/css/main.css': 'src/scss/main.scss'
babel: {
options: {
sourceMap: true,
presets: ['@babel/preset-env']
main: {
files: {
'dist/js/app.js': 'src/js/app.js'
watch: {
js: {
files: ['src/js/*.js'],
tasks: ['babel']
css: {
files: ['src/scss/*.scss'],
tasks: ['sass']
// grunt.loadNpmTasks('grunt-sass')
loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
grunt.registerTask('default', ['sass', 'babel', 'watch'])
// gulpfile.js
const { src, dest } = require('gulp')
const cleanCSS = require('gulp-clean-css')
const rename = require('gulp-rename')
exports.default = () => {
return src('src/*.css')
.pipe(cleanCSS())
.pipe(rename({ extname: '.min.css' }))
.pipe(dest('dist'))
通过给script添加type="module"就可以以ES Module的标准执行其中的JS代码;
ESM自动采用严格模式,忽略 'use strict'(不能全局使用this);
每个ESM都是运行在单独的私有作用域中;
ESM是通过CORS的方式请求外部JS模块的;
ESM的script标签会延迟执行脚本;
ES Modules导出导入: export / import。
热更新模块:browser-sync;
ES Modules 导出的注意事项:
export / import后面的{}是固定语法,与es6字面对象和结构无关;
export导出的是变量的引用,不是变量的复制;
export导出的是变量是只读的,不可在模块外部修改;
ES Modules 导入的注意事项:
import from后的路径和名称必须是完整的,不可省略.js和目录路径;
相对路径的话不可省略./,且可使用绝对路径或者完整的url;
import './module.js'只执行相关的模块文件,不导入变量;
import * as modObj from './module.js' 可以全部导出;
import不可嵌套在函数中,不可from一个变量,如有此动态加载需求,需使用:
import('.module.js').then(function(module){
console.log(module);
若export同时导出命名成员和默认成员,import可以使用如下方式接收:
// import {name, age, default as title} as modObj './module.js'
import title, {name, age} as modObj './module.js'
Webpack默认从src下的index.js打包到dist下的main.js,可在根目录添加webpack.config.js进行自定义:
// webpack.config.js
const path = require('path')
module.exports = {
// mode有:development/production/none
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'dist/'
Webpack 资源模块加载:加载除了js外的资源模块,需要使用module加载loader,Loader是Webpack的核心特性。
// webpack.config.js
module: {
rules: [
test: /.css$/,
use: [
'style-loader',
'css-loader'
// 小文件使用Data URLS,减少请求次数 ---> url-loader
// 大文件单独存放,提高加载速度 ---> file-loader
limit: 10 * 1024 // 10 KB
webpack加载资源的方式支持遵循ESM的import、CommonJs的require、AMD的define和require、css-loader中的@import和url、html代码中的img:src和a:herf。
对于同一个资源,可以依次使用多个loader,Loaders类似一个管道,最终接收的是一个javascript格式的字符串。
Webpack 插件机制 :Loader专注于实现资源模块加载,而Plugin解决其他自动化工作,Plugin拥有更宽的能力范围。
常用的一些webpack插件:
自动清除输出目录插件:clean-webpack-plugin
自动生成Html插件:html-webpack-plugin
复制文件插件:copy-webpack-plugin
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
template: './src/index.html'
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
new CopyWebpackPlugin([
// 'public/**'
'public'
自己实现webpack插件,需要通过在生命周期的钩子中挂载函数实现扩展,如要实现一个去除打包后js文件中的开头的注释的插件MyPlugin:
// webpack.config.js
class MyPlugin {
apply (compiler) {
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
// console.log(name)
// console.log(compilation.assets[name].source())
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {},
module: {
rules: []
plugins: [
new MyPlugin()
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
changeOrigin: true
Webpack HMR除了css文件外,需要手动去入口文件处理热更新逻辑:
module.hot.accept('./better.png', () => {
img.src = background
console.log(background)
配置文件根据环境导出不同配置,webpack.config.js导出一个函数:
module.exports = (env, argv) => {
const config = {}, // 放一些公共配置
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
new webpack.HotModuleReplacementPlugin()
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
return config
// 例如此为生产环境配置文件:webpack.prod.js
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
Webpack会在生产模式下自动开启Tree Shaking 来去除冗余代码,而在开发环境中,可在配置文件中使用:
optimization: {
// 模块只导出被使用的成员(标记无用代码)
usedExports: true,
// 压缩输出结果(删掉无用代码)
minimize: true
// 尽可能合并每一个模块到一个函数中
concatenateModules: true
Tree Shaking使用的前提是,由webpack打包的代码必须使用ESM;因此有可能与babel冲突失效,解决方法是babel-loader的配置选项指定modules。
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }]
动态导入(动态导入的模块会被自动分包)
if (hash === '#posts') {
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
} else if (hash === '#album') {
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
Webpack 输出文件名 Hash模式有三种:hash、chunkhash、contenthash:
output: {
filename: '[name]-[contenthash:8].bundle.js'
// rollup.config.js
import json from 'rollup-plugin-json'
import resolve from 'rollup-plugin-node-resolve'
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'iife'
plugins: [
json(),
resolve()
// 运行:yarn rollup --config
Stylelint用于检查CSS代码格式,有cli工具,支持sass/less/postCss,支持Gulp/Webpack:
// .stylelintrc.js
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-sass-guidelines'
Husky可以实现Git Hooks的使用需求,如提交前的eslint校验,lint-staged可以实现Husky后的代码格式化,二者在package.json中进行配合和配置。
第三阶段:Vue.js框架源码与进阶
模块一:手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法
任务一:Vue.js 基础回顾
vue基础结构:
new Vue({
data: {
company: {
name: '拉勾',
address: '中关村创业大街籍海楼4层'
render(h) {
return h('div', [
h('p', '公司名称:' + this.company.name),
h('p', '公司地址:' + this.company.address)
}).$mount('#app')
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')
const app = express()
// 注册处理 history 模式的中间件
app.use(history())
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')))
// 开启服务器,端口是 3000
app.listen(3000, () => {
console.log('服务器开启,端口:3000')
_Vue = Vue
//3 把创建Vue的实例传入的router对象注入到Vue实例
// _Vue.prototype.$router = this.$options.router
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
constructor(options) {
this.options = options
this.routeMap = {}
// observable
this.data = _Vue.observable({
current: "/"
this.init()
init() {
this.createRouteMap()
this.initComponent(_Vue)
this.initEvent()
createRouteMap() {
//遍历所有的路由规则 吧路由规则解析成键值对的形式存储到routeMap中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
initComponent(Vue) {
Vue.component("router-link", {
props: {
to: String
render(h) {
return h("a", {
attrs: {
href: this.to
on: {
click: this.clickhander
}, [this.$slots.default])
methods: {
clickhander(e) {
history.pushState({}, "", this.to)
this.$router.data.current = this.to
e.preventDefault()
// template:"<a :href='to'><slot></slot><>"
const self = this
Vue.component("router-view", {
render(h) {
// self.data.current
const cm = self.routeMap[self.data.current]
return h(cm)
initEvent() {
window.addEventListener("popstate", () => {
this.data.current = window.location.pathname
Object.keys(data).forEach(key => {
// 把 data 中的属性,转换成 vm 的 setter/setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('get: ', key, data[key])
return data[key]
set (newValue) {
console.log('set: ', key, newValue)
if (newValue === data[key]) {
return
data[key] = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data[key]
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
target[key] = newValue
document.querySelector('#app').textContent = target[key]
constructor () {
// { 'click': [fn1, fn2], 'change': [fn] }
this.subs = Object.create(null)
// 注册事件
$on (eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
// 触发事件
$emit (eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler()
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 注册模块
// 参数:数组,模块
// 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
let patch = init([
style,
eventlisteners
// 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h('div', {
style: {
backgroundColor: 'red'
on: {
click: eventHandler
h('h1', 'Hello Snabbdom'),
h('p', '这是p标签')
function eventHandler () {
console.log('点击我了')
let app = document.querySelector('#app')
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNde
let oldVnode = patch(app, vnode)
vnode = h('div', 'hello')
patch(oldVnode, vnode)
(2) 首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引,有四种情况 :
如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同) ,调用 patchVnode() 对比和更新节点,把旧开始和新开始索引往后移动 oldStartIdx++ / newStartIdx++;
如果 oldEndVnode和 newEndVnode 是 sameVnode (key 和 sel 相同) ,调用 patchVnode() 对比和更新节点,把旧结束和新结束索引往前移动 oldEndIdx-- / newEndIdx--;
如果 oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同,调用 patchVnode() 对比和更新节点,把 oldStartVnode 对应的 DOM 元素,移动到右边,更新索引;
如果 oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同,调用 patchVnode() 对比和更新节点,把 oldEndVnode对应的 DOM 元素,移动到左边,更新索引;
(3) 如果不是以上四种情况:
遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点;
如果没有找到,说明 newStartNode 是新节点,创建新节点对应的 DOM 元素,插入到 DOM 树中;
如果找到了,判断新节点和找到的老节点的 sel 选择器是否相同,如果不相同,说明节点被修改了,重新创建对应的 DOM 元素,插入到 DOM 树中 ,如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束;
新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束;
如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除;
不带 key 的情况需要进行两次 DOM 操作,带 key 的情况只需要更新一次 DOM 操作(移动 DOM 项),所以带 key 的情况可以减少 DOM 的操作。
模块二: Vue.js 源码分析(响应式、虚拟 DOM、模板编译和组件化)
任务一: Vue.js 源码剖析-响应式原理