Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

模块化开发与规范标准 #30

Open
janeLLLL opened this issue Nov 6, 2020 · 0 comments
Open

模块化开发与规范标准 #30

janeLLLL opened this issue Nov 6, 2020 · 0 comments

Comments

@janeLLLL
Copy link
Owner

janeLLLL commented Nov 6, 2020

模块化:对代码按照功能的不同去划分不同的模块

模块化演变过程

早期:

  1. 文件划分方式
    • 污染全局作用域
    • 命名冲突问题
    • 无法管理模块依赖关系
  2. 命名空间方式
  3. IIFE(立即执行函数)

模块化规范

构成:模块化标准 + 模块加载器

  1. CommonJS规范:以同步模式加载模块,启动时加载,执行不用加载(缺点)
  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过module.exports导出成员
  • 通过require函数载入模块

编译后的文件名如果是vue.runtime.js,模块化采用的是CommonJS

  1. 为浏览器设计新规范:AMD(Asynchronous Module Definition)
require([./module1], function(module1){
    module1.start()
})
  • 使用复杂
  • 模块js文件请求频繁
  1. Sea.js + CMD
define(function (require, exports, module){
    var $ = require('jquery')
    module.exports = function () {
        console.log('hi')
        $('body').append('<p>hi</p>')
    }
})
  1. ES Modules + CommonJS:模块化最佳实践方式

Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

ES Modules

基本特性

通过script添加 type = "module" 的属性,就可以以 ES Module 的标准执行其中的JS代码。使用:

<script type = "module">
    console.log('hi')
</script>
serve .//npm工具去启动
  1. ESM自动采用严格模式,忽略'use strict'

    • this不指向全局对象
  2. 每个ES Module都是运行在单独的私有作用域中

  3. ESM是通过 CORS 的方式请求外部 JS 模块的

    • 如果src标头不支持 CORS,会报跨域的错误
  4. ESM 的 script标签会延迟执行脚本

    • 脚本写在标签之前,脚本完成之后才会显示对应的标签

导入 / 导出

export / import

//./module.js
const foo = 'es modules'
export {foo as fooName}//重命名
export {foo as default}


//./app.js
import { fooName } from './module.js'
import { defaul tas fooName } from './module.js'//必须要指定变量名
console.log(fooName)
  1. 导出:
  • 导出不是字面量

    如果要导出一个字面量:

    export default {name,age}
  • 暴露出来的是一个引用关系,只读

  1. **导出:**导出时文件路径要填写完整
  • 导入一些不需要外界控制的子模块

    import {} from './module.js'
    import './module.js'
  • 把所有导出成员都导出来

    import * from './module.js'
  • 动态导入模块机制:import...from...只能出现在最顶层

    import('./module.js')//返回的是一个promise,promise是异步加载
    	.then(function(module){
        console.log('hi')
    })
  • 提取模块中默认成员写法

    import title from './module.js'
    //等价于import {default as title} from './module.js'
  1. 导出导入成员:如果将import改成export,那么这些成员将作为目前模块的导出成员看待;同时,目前模块也将无法访问到该导出成员

    //import { foo } from './module.js'
    export { foo } from './module.js'

    实例:当需要导入的模块过多时,可以新建一个中转文件index.js,把组件导入再导出;default参数必须要进行重命名

    //./index.js
    export { Button } from './button.js'
    export { Avatar } from './avator.js'
    export { default as newB } from './button.js'

ESM浏览器环境Polyfill

IE不兼容ESM标签:引用browser-es-module-loader去使用ESM

  • 引入脚本文件到ESM中:cdn服务 unpkg.com/browser-es-module-loader 拿到js文件

    <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
      <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
      <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
    • 因为在支持的浏览器中,browser-es-module-loader会重复加载,使用 nomodule 属性使其只在不支持的浏览器中工作
    • 在生产阶段不要这样用,动态解析脚本会使工作效率变差

    ESM运行原理:

    1. 将浏览器不识别的ESM交给babel转换
    2. 需要import的文件通过ajx请求回来的代码再通过babel转换

ESM in node.js(过渡状态)

  1. 支持情况
  • 使生产阶段也可以在不同浏览器使用ESM

使用:

1. .js => .mjs
2. node --experimental-modules index.js

不支持第三方模块导出默认成员,因为第三方模块并没有向外去暴露出一个成员/第三方模块都是导出默认成员

import { camelCase } from 'lodash'
//错误的写法
  1. 与CommonJS交互

    • ESM可以导入CJS模块

    • CJS不能导入ESM模块

    • CJS始终只会导出一个默认成员

      import { foo } from './common.js'
      //foo无法提取
    • 注意:import不是解构导出对象(只是一个固定的用法

  2. 与CommonJS模块的差异

    //cjs
    //加载模块函数
    console.log(require)
    
    //模块对象
    console.log(module)
    
    //导出对象别名
    console.log(exports)
    
    //当前文件的绝对路径
    console.log(__filename)
    
    //当前文件所在目录
    console.log(__dirname)
    • ESM中没有CJS的那些模块全局成员了

新版本支持

//package.json
{
	"type":"module"
}
xx.mjs => xx.js
common.js => common.cjs

常用的模块化打包工具 / Webpack打包

由来 / 概要

几种模块化对生产环境产生的影响:

  1. ESM存在环境兼容问题
  2. 模块文件过多,网络请求频繁
  3. 所有前端资源都需要模块化

功能:

  • 新特性代码编译
  • 模块化JavaScript打包
  • 支持不同类型的资源模块

概要

模块打包器(module bundler):将零散的代码打包到JS文件中

模块加载器(loader):将兼容有问题的代码编译转换

代码拆分(code splitting):挑选有需要的代码打包。当实际中需要到某个模块,再通过异步去加载它,实现增量加载或者渐进式加载

资源模块(asset module):webpack支持以模块化载入任意的资源文件,例如,JavaScript中可以支持import CSS文件

模块化工具的作用:打包工具解决的是前端整体的模块化,并不单指JavaScript模块化

上手

yarn init --yes
yarn add webpack webpack-cil --dev
//引入webpack模块
yarn webpack
//webpack从index.js开始打包
//打包结果会存放在dist目录里

//可以把webpack命令放在package.json中简化打包过程
"scripts":{
	"build":"webpack"
}
yarn build

配置文件

约定:入口文件src/index.js -> dist/main.js

自定义:src/main.js

  1. 在根目录下添加webpack.config.js

    const path = require('path')
    module.exports = {
    	entry:'./src/main.js',
        output:{
            filename:'bundle.js',
            //path:'output'
            path:path.join(__dirname,'output')
            //指定输出文件所在的目录
            //都需要使用绝对路径
        }
    }

    Node.js的__dirname,__filename,process.cwd(),./的一些坑 jawil/blog#18

    • __dirname: 总是返回被执行的 js 所在文件夹的绝对路径
    • __filename: 总是返回被执行的 js 的绝对路径
    • process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径
    • ./: 跟 process.cwd() 一样,返回 node 命令时所在的文件夹的绝对路径

工作模式

yarn webpack
//默认使用prouction模式工作:webpack会启动一些优化插件,自动压缩代码
yarn webpack --mode development
//添加一些调试过程的辅助到代码当中
yarn webpack --mode none
//原始状态打包

也可以在配置中添加此属性

//webpack.config.js
const path = require('path')
module.exports = {
    mode:'development',
	...
}

打包结果运行原理

模块私有作用域

webpack资源模块加载

  • Loader是Webpack的核心特性,借助不同的Loader可以加载任何类型的资源

默认不解析css文件,需要用适当的加载器loader去处理此类型资源文件,使得css文件转换成一个js模块

  1. 安装css-loader

    yarn add css-loader --dev
    yarn add style-loader --dev//将转换的js结果通过style标签追加到页面上
    
  2. 添加加载资源规则配置

    //webpack.config.js
    module: {
        rules:[
            {
                test:/.css$/,
                use:[
                    'style-loader',
                    'css-loader'
                    //从后往前执行
                ]
            }
        ]
    }

webpack导入资源模块

  • 打包入口 ≈ 运行入口

  • JavaScript驱动整个前端应用的业务

webpack建议在js引入css文件,原因:

  • 根据代码的需要动态导入资源

  • 需要资源的不是应用,而是代码

import './xxx.css'

意义:

  • 逻辑合理,js确实需要这些资源文件
  • 确保上限资源不缺失,都是必要的

文件资源加载器(file-loader)

示例:

  1. 需要转换成js文件
import icon from './icon.png'

const img = new Image()
img.src = icon

document.body.append(img)//到body中
//webpack.config.js
module: {
    rules:[
        ...
        {
            test:/.png$/,
            use:'file-loader'
        }
    ]
}
yarn webpack
  1. 配置最终网站文件在根目录的位置,才能正常显示网站图片
//webpack.config.js
output:{
    ...
    publicPath:'dist/'
}

Data URLs 与 url-loader

  • Data URLs:以代码的形式直接表示一个文件

  • 不需要独立的物理文件了

  • 合适打包体积小的资源,体积过大打包文件就过大,运行时间长

    image-20200927142803147

使用:

  1. 安装url-loader

    yarn add url-loader --dev
    
  2. 配置

    //webpack.config.js
    {
        test:'/.png$/',
        use:{
            loader:'url-loader',
            options:{
                limit: 10 * 1024
                //只处理10kb以下的文件!还是要安装file-loader模块
            }
        }
    }

优化:

小文件使用Data URLs,减少请求次数

大文件单独提取,提高加载速度

常用加载器分类

  1. 编译转换类:css-loder以JS形式工作的css模块
  2. 文件操作类:flie-loader导出文件访问路径
  3. 代码检查类:eslint-loader检查通过/不通过

webpack编译ES6

!webpack不会自动编译ES6,因为模块打包需要,才会处理import和export

  • webpack只是打包工具
  • 加载器可以用来编译转换代码
  • 使用babel-loader编译代码

    yarn add babel-loader @babel/core @babel/preset-env --dev
    
    //webpack.config.js
    rules: [
        {
            test: /.js$/,
            use: {
                loader:'babel-loader',
                options:{
                    presets:['@babel/preset-env']
                    //env插件集合,包括了全部的ES特性
                }
            }
        }
    ]

模块加载方式

webpack兼容多种模块化标准:

  1. 遵循ES Modules标准的import声明

    import './xxx.css'
  2. 遵循CommonJS标准的require函数

    const createHeading = require('./xx.js').default
  3. 遵循AMD标准的define函数和require函数

    define(['./heading.js','./main.css'], (createHeading, icon) => {
        ...
    })
    require(['./heading.js','./main.css'], (createHeading, icon) => {
        ...
    })
  • 以上最好不要混合用
  • loader加载的非JavaScript也会触发资源加载,样式代码中的@import指令url函数
  • HTML代码中图片标签的src属性

所有需要引用资源的地方都会被webpack找出来,根据不同的配置交给不同的loader处理,最后将处理的结果整体打包到输出目录

webpack就这样完成项目的模块化

核心工作原理

  • 依赖树

    image-20200927161203060

    webpack递归此依赖树,找到对应节点资源文件,根据rule使用加载器加载这个模块,最后的结果放到bundle.js中

  • loader机制是webpack的核心

开发一个loader

  • loader负责资源文件从输入到输出的转换
  • loader ≈ 管道
  • 对于同一个资源可以依次使用多个loader

示例:css-loader → style-loader

webpack插件机制 / plugin

  1. loader专注实现资源模块加载

  2. plugin解决其他自动化工作:清除dist目录、拷贝静态文件至输出目录、压缩输出代码

自动清除输出目录插件 / clean-webpack-plugin

yarn add clean-webpack-plugin --dev
//webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

plugin: [
    new CleanWebpackPlugin()
]

每次执行前就可以清除dist目录的文件了

自动生成html插件 / html-webpack-plugin

根目录里有index.html文件,每次部署都需要上次dist文件和index.html,我们可以直接使用webpack打包html文件,删除根目录文件

  • 使用插件
  • 自定义输出内容
  • 同时输出多个页面文件

自动生成使用bundle.js的html

yarn add html-webpack-plugin --dev
//webpack.config.js
const htmlWebpackPlugin = require('html-webpack-plugin')
//不需要解构

output: {
    filename:'bundle.js',
    path: path.join(__dirname, 'dist')
    //publicPath: 'dist/'
}

plugin: [
    //用于生成index.html
    new htmlWebpackPlugin({
        title:'Webpack Plugin Sample',
        //html标题
        meta:{
            viewport:'width=device-width'
            //对象的形式设置页面元素标签
        }
        /**
        * 自定义输出内容 
        */
    }),
    //用于生成about.html
    new HtmlWebpackPlugin({
        filename:'about.html',
    })
]
yarn webpack

最后会输出一个index.html文件到打包目录

静态文件拷贝 / copy-webpack-plugin

将public里的文件完整拷贝到输出目录

yarn add copy-webpack-plugin --dev
//webpack.config.js
const copyWebpackPlugin = require('copy-webpack-plugin')
//不需要解构

plugin: [
	...
    new copyWebpackPlugin([
        'public'
    ])
]
yarn webpack

loader只加载模块,plugin拥有更宽的能力范围

插件机制的工作原理

  • plugin通过钩子机制实现

    钩子类似于web中的事件,为了便于插件扩展,webpack给每一个环节都埋下了钩子Honk。开发插件时,往不同的节点挂载不同的任务,就可以扩展webpack的能力

  • webpack要求插件必须是一个函数或者一个包含apply方法的对象

    ////webpack.config.js
    class MyPlugin {
        /**
        * compiler对象包含了此次所有的配置信息
        */
        apply(compiler) {
            console.log('MyPlugin启动时调用')
            
            //名称1 + 挂载到钩子的函数2
     compiler.hooks.emit.tap('MyPlugin', compilation => {
                //compilation此次打包的上下文
         for (const name in compilation.assets){
             /**
             * name 文件名
             * compilation.assets[name].source() 文件内容
             */
             if(name.endsWith('.js')){
                 //判断文件名是否以.js结尾
                 const contents = compilation.assets[name].source()
                 const withoutComments = contents.replace(/\/\*\*+\*\//g,'')//全局替换注释
             }
             compilation.assets[name] = {
                 source: () => withoutComments,
                 size: () => withoutComments.length
             }
         }
            })
        }
    }
    
    //-----使用------
    plugins:[
        new MyPlugin()
    ]

eg:清除bundle.js中不必要的注释

​ 借用emit()钩子

实现:通过在生命周期的钩子中挂载函数实现扩展

理想的开发环境

  1. 以HTTP Server运行
  2. 自动编译 + 自动刷新
  3. 提供Source Map支持

自动编译

  • watch工作模式:监听文件变化,自动重新打包

    yarn webpack --watch
    

自动刷新浏览器

  • BrowserSync自动刷新功能

    browser-sync dist --files "**/*"
    
  • 缺:麻烦、效率低

Webpack Dev Server

  • Webpack Dev Server提供用于开发的HTTP Server:集成自动编译和自动刷新浏览器

    yarn add webpack-dev-server
    

    特点:为了工作效率没有将结果打包到磁盘中,减少磁盘的读写操作

    • 静态文件:开发阶段不需要参与webpack构建,但同样需要被served

    • 默认只会serve打包输出文件

    • 只要是webpack输出的文件,都可以直接被访问

    • 其它资源文件也需要serve

    //webpack.config.js
    module.exports = {
        ...
        devServer: {
            contentBase: './public'
            //可以额外为开发服务器指定查找资源目录
        }
    }
    
    plugins: [
        //开发阶段最好不要使用这个插件:静态文件拷贝
        //new CopyWebpackPlugin(['public'])
    ]

代理API

  • 解决开发环境接口跨域请求问题

    域名协议端口不一致

  • 解决方式1:跨域资源共享(CORS),使用的CORS的前提是API必须支持,并不是任何情况下API都应该支持。如果同源部署就没有必要使用CORS

  • 解决方式2:Webpack Dev Server支持配置代理

    image-20200929101529967

    • 目标:将github API代理到开发服务器

      Endpoint可以理解为接口端点/入口

      devServer: {
          proxy:{
              '/api':{
                  //http://localhost:8080/api/users 相当于请求 http://api.github.com/api/users
                  target:'http://api.github.com',//代理目标
                  //http://localhost:8080/api/users 相当于请求 http://api.github.com/users
                  pathRewrite: {
                      '^/api':''
                  },
                  //不能使用localhost:8080作为请求github的主机名
                  changeOrigin: true
              }
          }
      }

      主机名是http协议中的相关概念

Source Map / 源代码地图

解决:运行代码与源代码之间完全不同,无法调试应用,错误信息无法定位。调试和报错都是基于运行代码

  • Source Map是一个独立的map文件,与源码在同一个目录下

    JavaScript Source Map 详解

    Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置

  • 运行代码通过Source Map逆向解析得到源代码

    例子:新版jquery.min.js文件手动添加注释

    //# sourceMappingURL = jquery-3.4.1.min.map
  • Source Map解决了源代码与运行代码不一致所产生的我问题

webpack配置Source Map

基本使用:

//webpack.config.js
devtool:'source-map'

bundle.js文件最后也自动加上注释

yarn webpack
sever dist
  • webpack支持12种不同的方式,每种方式的效率和效果各不同

eval模式下的Source map

eval是js中的一个函数

控制台中的eval运行在虚拟机中,可以使用sourcelURL声明这段代码所属的文件路径

设置成eval模式:

//webpack.config.js
devtool:'eval'
  • 浏览器知道这段代码所对应的文件,从而实现错误定位的文件,这种模式下不会生成source map文件,构建速度最快,但效果简单
  • 只能定位源代码的名称而不能定位行列和信息

webpack devtool模式对比

eval:只能定位哪一个文件除了错误

eval-source-map:同样使用eval模式执行代码,可以定位到行和列的信息。相比eval生成了source-map

cheap-eval-source-map:只能定位到行信息,快一点,显示ES6转换后的代码

cheap-module-eval-source-map:也定位到行,但显示的是我们写的源代码没有经过loader加工

inline-source-map:普遍的source-map文件都是以物理文件存在,而该模式是使用dataURL的方式嵌入到代码中。代码体积大很多,不常用

hidden-source-map:构建当中生成了该文件但不展示

nosources-source-map:没有源代码但是提供了行列信息。以上两者保护生产环境代码不被暴露

eval是否使用eval执行模块代码

cheap-source-map是否包含行信息

module是否能够得到loader处理之前的源代码

如何选择Source Map模式(个人)

  • 开发模式:cheap-module-eval-source-map

  • 生产模式:不生成source-map

  • 调式阶段:nosources-source-map,定位到源代码位置,不至于向外暴露你源代码的内容

模块热替换 HMR

自动刷新:不丢失当前页面状态

HMR集成在webpack-dev-server

webpack-dev-server --hot

或者在配置文件中开启

//webpack.config.js
const webpack = require('webpack')
const HtmlWebpack = require('html-webpack-plugin')
//载入webpack内置插件

devServer: {
	hot: true
}
plugin: [
    new webpack.HotModuleReplacementPlugin()
]
yarn webpack-dev-server --open

特点

  1. 不可以开箱即用:需要手动处理模块热替换逻辑
  2. 样式文件的热更新开箱即用:样式文件经过loader处理的
  3. 使用了某个框架可以自动热更新,框架下的开发,每种文件都有规律
  4. 通过脚手架创建的项目都集成了HMR方案
  • 我们需要手动处理JS模块更新后的热替换

HMR APIs

  • 为JS提供了一套处理HMR的API,处理当某一个模块更新后该如何替换到页面
//main.js
//如果对这个模块进行处理了就不会自动刷新
module.hot.accept('./editor', () => {
    //editor更新需要在这里手动处理热替换逻辑
})

webpack处理js模块热替换

eg:针对editor模块处理热替换

//main.js
let lastEditor = editor
module.hot.accept('./editor', () => {
    const value = lastEditor.innerHTML
    //先保存状态
    document.body.removeChild(editor)
    //移除元素
    const newEditor = createEditor()//创建一个新的元素追加到页面当中
    newEditor.innerHTML = value
    //再设置新元素状态
    document.body.appendChild(newEditor)
})

更加通用的热替换:图片模块热替换

//main.js
//注册这个图片的热替换处理函数
const img = new Image()

module.hot.accept('./better.png', () => {
    img.src = background
    //设置为新的src就可以了
})
  • 要写一些额外的代码但是利大于弊

HMR注意事项

  1. 处理HMR的代码报错会导致自动刷新:使用hotOnly

  2. 没启用HMR的情况下,HMR API报错

    因为没有module.hot这个对象

    if(module.hot){
    	...业务代码
    }
  3. 代码中多了一些与业务无关的代码,但不会影响生产环境

生产环境优化

生产环境注重运行效率——模式(mode)

为不同的工作环境创建不同的配置:

  1. 配置文件根据环境不同导出不同配置

    webpack支持导出一个函数,这个函数返回一个配置对象

    //webpack.config.js
    //env-通过cli传递的环境名参数,argv-运行cli传递的所有参数
    module.exports = (env, argv) => {
        const config = {
            //开发模式
            mode:'development',
            ...
        }
        
        if(env === 'production'){
            config.mode = 'production'
            config.devtool = false//禁用掉source map
            config.plugins = [
                ...config.plugin,
                new CleanWebpacPlugin()
                new CopyWebpackplugin(['public'])
                //开发阶段的插件,只有在上限打包之前才有自己的价值
                return config
            ]
        }
    }
    yarn webpack --env production
    
  2. 不同环境对应不同配置文件

公共配置:

//webpack.common.js
//直接把webpack.dev.js复制过来

生产环境配置:

//webpack.prod.js
const common = require('./webpack.common')
//导入公共配置
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

//把公共对象复制到其中,并且可以覆盖掉公共模块中的一些配置
module.exports = merge(common, {
    mode: 'production',
    plugins: [
        new CleanWebpackPlugin()
        new CopyWebpackPlugin(['public'])
    
    ]
})
//需要合并webpack配置的需求
yarn add webpack-merge --dev

运行webpack的时候需要--config添加指定的文件名

yarn webpack --config webpack.prod.js

命令也可以定义到package.json中

//package.json
"scripts": {
    "build": "webpack --config webpack.prod.js"
}
yarn build

Webpack DefinePlugin

  • 为代码注入全局成员

    process.env.NODE_ENV
    //第三方模块都是针对这个成员去判断当前的运行环境,决定是否去执行打印日志等操作
    
//webpack.config.js
const webpack = require('webpack')

module.exports = {
    plugin: [
        new webpack.DefinePlugin({
            //API_BASE_URL: '"http://api.example.com"'
            //为代码注入一个API服务地址
            API_BASE_URL: JSON.stringify('http://api.example.com')
            //可以先转换成表示这个值的代码片段
        })
        //此对象每一个键值都会注入到代码中
    ]
}
//main.js
console.log(API_BASE_URL)//http://api.example.com

Tree-shaking

  • 摇掉代码中未引用的部分 / 未引用代码(dead-code)

  • 比如一些console.log语句

    yarn webpack --mode production
    

    冗余代码并没有输出

    Tree-shaking不是指某个配置选项,是一组功能搭配使用后的优化效果

    production模式下自动开启

    在其它模式开启的办法:

    //webpack.config.js
    module.exports = {
        mode: 'none',
        ...
        optimization: {
            usedExports: true,
            //在导出里只导出外部使用的成员 - 负责标记”枯树叶“
            minimize: true
            //未引用的代码都被移除掉了 - 负责摇掉
        }
    }

webpack合并模块 / Scope Hoisting

  • 作用域提升

  • 尽可能将所有模块合并输出到一个函数中

  • 既提升了运行效率,又减少了代码体积

Tree Shaking与Babel

  • Tree Shaking前提是ES Modules

  • 由webpack打包的代码必须使用ESM

  • 为了转换代码中的ECMAScirpt新特性,会使用babel-loader处理JS,将ESM 转换为 CommonJS

    //webpack.config.js
    module: {
        rules: [
            test: /\.js$/,
            use: {
            loader: 'babel-loader',
            options: {
            presets: [['@babel/preset-env', {modules: 'commonjs'}]]
        //强制配置babel
            }
            }
        ]
    }
  

## sideEffects

- 确保你的代码没有副作用

  ```json
  //webpack.json
  "sideEffects": [
      './src/extend.js',
      '*.css'
  ]
  • bundle.js里有副作用的模块都被打包进来了

code-splitting / 代码分包-代码分割

  • 所有代码最终都被打包到一起,造成bundle体积过大
  • 但并不是每个模块在启动时都是必要的
  • 分包,按需加载

按照不同的规则打包到不同的bundle,从而提高应用的响应速度

多入口打包

  • 适用于多页应用程序,一个页面对应一个打包入口,公共部分单独提取

  • 配置entry

    //webpack.config.js
    entry: {
        index: './src/index.js',
            album: './src/album.js'
    }
    //对象中,一个属性就是一个打包入口
    output: {
        filename: '[name].bundle.js'
    }
    //动态替换成入口文件名
    plugin: [
        new HtmlWebpackPlugin({
            ...
            filename: 'index.html'
            chunks:['index']
        })
        new HtmlWebpackPlugin({
            ...
            filename: 'index.html',
            chunks:['album']
        })
    ]
    //为两个页面配置两个不同的chunk

    我们期待一个页面使用一个对应结果,配置chunk之后会输出两个页面index.html / album.html

    yarn webpack
    
  • 问题: 不同入口中肯定有公共模块被import

提取公共模块

//webpack.config.js
optimization: {
    splitChunks: {
        chunks: 'all'
    }
}
yarn webpack

会生成两个相同模块的公共部分album~index.html.js

动态导入

  • 按需加载,需要用到某个模块时,再加载这个模块

  • 动态导入的模块会被自动分包

    //index.js
    if(hash === '#posts'){
        import('./posts/posts').then(({default: posts}) => {
        mainElement.appendChild(posts())
            //创建界面上的元素
    })
    }else if(hash === '#album') {
        import('./album/album').then(({default:album}) => {
            mainElement.appendChild(album())
        })
    }

魔法注释

  • 给bundle分包进行命名

    //index.js
    if(hash === '#posts'){
        import(/* webpackChunkName:'posts'  */,'./posts/posts').then(({default: posts}) => {
        mainElement.appendChild(posts())
            //创建界面上的元素
    })
    }else if(hash === '#album') {
        import(/* webpackChunkName:'album'  */,'./album/album').then(({default:album}) => {
            mainElement.appendChild(album())
        })
    }
  • 生成的bundle就会用注释的名称起名

MiniCssExtractPlugin

  • 提取CSS到单个文件

    //webpack.config.js
    const MiniCssExtractPlugin = require('html-webpack-plugin')
    module: {
        rules: [
            test: /\.css$/,
            use: [
            MiniCssExtractPlugin.loader,
            //将样式通过style标签注入
        ]
        ]
    }
  • css超过50kb才考虑单独提取

OptimizeCssAssetsWebpackPlugin

  • 压缩输出的CSS文件

    yarn add optimize-css-assets-webpack-plugin --dev
    
    //webpack.config.js
    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    
    plugin: [
        new OptimizeCssAssetsWebpackPlugin()
    ]
  • 官方建议压缩插件配置到minimizer

    //webpack.config.js
    optimization: {
        minimizer: [
            new OptimizeCssAssetsWebpackPlugin()
        ]
    }
    yarn webpack --mode production
    
  • 此时,默认JS文件不被压缩了,要重新配置内置的terser-webpack-plugin

    yarn add terser-webpack-plugin --dev
    
    //webpack.config.js
    const TerserWebpackPlugin = require('terser-webpack-plugin')
    
    optimization: {
        minimizer: [
            new TerserWebpackPlugin()
        ]
    }
  • JS和CSS文件都被压缩了

输出文件名Hash

  • 部署文件,会启动静态资源缓存,对于用户的浏览器而言,会缓存住静态资源,后续就不用再请求服务器得到静态资源文件。

  • 不过,开启客户端的静态资源缓存,如果在缓存策略中,缓存失效时间过短,效果不会很明显,时间过长,更新无效

  • 建议,生成模式下,文件名使用Hash,一旦资源文件发生改变,文件名称也会发生变化

    //webpack.config.js
    output / MiniCssExtractPlugin : 
    	//filename: '[name]-[hash].bundle.xx'
    filename: '[name]-[contenthash:8].bundle.xxx'
    //根据输出文件内容输出哈希值,不同的文件就有不同的哈希值,最适合解决缓存问题

    image-20201006001838599

Rollup

介绍

  • ESM打包器
  • 更为小巧
  • 不支持HMR
  • 提供充分利用ESM各项特性的高效打包器

上手

yarn add rollup --dev

参数指定打包入口文件 + 输出格式 + 输出文件路径

yarn rollup ./src/index/js --format iife --file dist

配置文件

//rollup.config.js
//导出一个配置对象
export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',//输出文件名
        format: 'iife',//输出格式
    }
}

需要指定配置文件读取:

yarn rollup --config

或者指定不同配置文件的名称

yarn rollup --config rollup.config.js

使用插件

扩展,且插件时rollup唯一扩展途径:

  1. 加载其他类型资源模块
  2. 导入CommonJS模块
  3. 编译ECMAScript新特性

e.g.rollup-plugin-json:加载json类型模块

yarn add rollup-plugin-json --dev
//rollup.config.js
import json from 'rollup-plugin-json'

plugin:[
    json()
    //将调用结果放在数组中
]

可以直接使用package.json中的参数

//index.js
import {name,version} from '../package.json'
log(name)

在打包结果bundle.js里就出现了name字段的参数

加载NPM模块

rollup-plugin-node-resolve:直接使用模块名称导入对应模块

yarn add rollup-plugin-node-resolve --dev
//rollup.config.js
import resolve from 'rollup-plugin-node-resolve'

plugin: [
    resolve()
]

直接可以导入npm模块:

//index.js
import _ from 'loadsh-es'
log(_.camelCase('111'))

加载commonJS

rollup-plugin-commonjs

//rollup.config.js
import commonjs from 'rollup-plugin-commonjs'

plugins: [
    commonjs()
]

新建cjs-module.js

module.exports = {
    foo: 'bar'
}
//index.js
import cjs from './cjs-module'//默认导出
log(cjs)

打包后,bundle.js以默认对象导出了结果

代码拆分

Dynamie Imports:动态导入,按需加载

Code Splitting:自动处理代码的拆分

//index.js
import('./logger').then({log}) => {
    //结构的方式提取log方法
    log('111')
}

不允许IIFE输出格式,要实现代码拆分必须使用amd(浏览器环境)或者commojs标准

yarn rollup --config --format amd
//覆盖掉文件的format设置

当输出多个文件时,不允许使用file模式

//rollup.config.js
export default {
    input: 'src/index.js',
    output: {
        dir: 'dist',
        format: 'amd'
    }
}
yarn rollup --config

多入口打包

//rollup.config.js
//方式1
input: ['src/index.js','src/album.js']
//方式2
input: {
    foo: 'src/index.js',
    bar: 'src/album.js'
}
//内部打包不会自动提取公共模块,所以format必须是amd模式
  • 对于amd输出格式,不能直接引用到页面上,必须要通过html标准库引用

新建dist/index.html

<sript src="foo.js"></sript>

Rollup / Webpack 选用规则

Rollup优势:

  1. 输出结果更加扁平
  2. 自动移除未引用代码
  3. 打包结果依然完全可读

缺点:

  1. 加载非ESM的第三方模块比较复杂
  2. 模块最终都被打包到一个函数中,无法实现
  3. 浏览器环境中,代码拆分功能依赖AMD库

webpack:开发应用程序

rollup:JavaScript框架、类库

Parcel

  • 零配置的前端应用打包器
  1. 初始化package.json
yarn init
  1. 安装parcel
yarn add parcel-bundle --dev
  1. 新建src/index.html编写开发阶段的源代码,也是打包的入口文件
<script src="main.js"></script>
  1. 新建main.jsfoo.js
//foo.js
export default {
    bar: () => {
        console.log('2')
    }
}
//main.js
import foo from './foo'
import $ from 'jquery'
import './style.css'
import logo from './zce.png'

$(document.body).append('<h1>hi</h1>')
//打包时可以自动安装jquery模块,也可以加载其它类型模块

//也可以支持资源模块动态导入
import('jquery').then($ => {
    $(document.body).append(`<img src="${logo}"/>`)
})

foo.bar()
//使用热加载
if(module.hot){
    module.hot.accept(() => {
        console.log('1')
    })
}
  1. 运行打包命令
yarn parcel src/index.html
  1. 以生产模式进行打包
yarn parcel build src/index.html
  • webpack相比有更好的生态

规范化标准

  1. 为什么

软件开发需要多人协同,不同开发者具有不同的编码习惯和喜好增加项目维护成本,需要明确一个统一的标准

  1. 哪些地方需要
  • 代码、文档、提交日志
  1. 实施规范化的方法
  • 编码前人为标准约定
  • 通过工具实现Lint

ESLint结合webpack

  • 最为主流的JavaScript Lint工具监测JS代码质量
  • 统一开发者的编码风格
  • 帮助开发者提升编码能力

ESLint工具使用

  1. 初始化项目

    • 创建管理依赖文件package.json
    npm init --dev
    
  2. 安装ESLint模块为开发依赖

    npm install eslint --save-dev
    npx eslint --version
    npx eslint --init
    
  3. 通过CLI命令验证结果

ESL检查步骤:

  1. 编写“问题代码”

  2. 使用eslint执行检测

    • 当语法出现问题时,eslint是不会校验语法规则和风格的
    npx eslint .\01.js --fix
    
  3. 完成eslint使用配置

ESLint配置文件解析

ESLint配置文件.eslintrc参数说明

深入理解 ESlint

  • eslint可以通过package.json的eslintConfig属性配置

  • parserOptions:ESLint 允许你指定你想要支持的 JavaScript 语言选项。默认情况下,ESLint 支持 ECMAScript 5 语法。你可以覆盖该设置,以启用对 ECMAScript 其它版本和 JSX 的支持。

  • parser:ESLint 默认使用 Espree 作为其解析器,你可以在配置文件中指定一个不同的解析器,只要该解析器符合下列要求:

  1. 它必须是一个 Node 模块,可以从它出现的配置文件中加载。通常,这意味着应该使用 npm 单独安装解析器包。
  2. 它必须符合 parser interface。
  • processor:插件可以提供处理器。处理器可以从另一种文件中提取 JavaScript 代码,然后让 ESLint 检测 JavaScript 代码。或者处理器可以在预处理中转换 JavaScript 代码。若要在配置文件中指定处理器,请使用 processor 键,并使用由插件名和处理器名组成的串接字符串加上斜杠。

  • env:一个环境定义了一组预定义的全局变量。

  • globals:当访问当前源文件内未定义的变量时,no-undef 规则将发出警告。如果你想在一个源文件里使用全局变量,推荐你在 ESLint 中定义这些全局变量,这样 ESLint 就不会发出警告了。你可以使用注释或在配置文件中定义全局变量。

  • plugins:ESLint 支持使用第三方插件。在使用插件之前,你必须使用 npm 安装它。

  • rules:ESLint 附带有大量的规则。你可以使用注释或配置文件修改你项目中要使用的规则


    为了在文件注释里配置规则,使用以下格式的注释:

/* eslint eqeqeq: "off", curly: "error" */

在这个例子里,eqeqeq 规则被关闭,curly 规则被打开,定义为错误级别。你也可以使用对应的数字定义规则严重程度:

/* eslint eqeqeq: 0, curly: 2 */

这个例子和上个例子是一样的,只不过它是用的数字而不是字符串。eqeqeq 规则是关闭的,curly 规则被设置为错误级别。

如果一个规则有额外的选项,你可以使用数组字面量指定它们,比如:

/* eslint quotes: ["error", "double"], curly: 2 */

这条注释为规则 quotes 指定了 “double”选项。数组的第一项总是规则的严重程度(数字或字符串)。


  • settings:ESLint 支持在配置文件添加共享设置。你可以添加 settings 对象到配置文件,它将提供给每一个将被执行的规则。如果你想添加的自定义规则而且使它们可以访问到相同的信息,这将会很有用,并且很容易配置。

  • extends:一个配置文件可以被基础配置中的已启用的规则继承。

定制ESLint校验规则

ESLint 附带有大量的规则,你可以在配置文件的 rules 属性中配置你想要的规则。每一条规则接受一个参数,参数的值如下:

  • "off" 或 0:关闭规则
  • "warn" 或 1:开启规则,warn 级别的错误 (不会导致程序退出)
  • "error" 或 2:开启规则,error级别的错误(当被触发的时候,程序会退出)

举个例子,我们先写一段使用了平等(equality)的代码,然后对 eqeqeq 规则分别进行不同的配置。

// demo.js
var num = 1
num == '1'

  • 这里使用了命令行的配置方式,如果你只想对单个文件进行某个规则的校验就可以使用这种方式:

    eqeqeq 规则校验

我们看下 quotes 规则,根据官网介绍,它支持字符串和对象两个配置项:

{
  "rules": {
    // 使用数组形式,对规则进行配置
    // 第一个参数为是否启用规则
    // 后面的参数才是规则的配置项
    "quotes": [
      "error",
      "single",
      {
        "avoidEscape": true 
      }
    ]
  }
}

扩展

扩展就是直接使用别人已经写好的 lint 规则,方便快捷。扩展一般支持三种类型:

{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "eslint-config-standard",
  ]
}
  • eslint: 开头的是 ESLint 官方的扩展,一共有两个:eslint:recommendedeslint:all
  • plugin: 开头的是扩展是插件类型,也可以直接在 plugins 属性中进行设置,后面一节会详细讲到。
  • 最后一种扩展来自 npm 包,官方规定 npm 包的扩展必须以 eslint-config- 开头,使用时可以省略这个头,上面案例中 eslint-config-standard 可以直接简写成 standard

如果你觉得自己的配置十分满意,也可以将自己的 lint 配置发布到 npm 包,只要将包名命名为 eslint-config-xxx 即可,同时,需要在 package.json 的 peerDependencies 字段中声明你依赖的 ESLint 的版本号。

ESLint对TypeScript支持

//.eslintrc.js
parser:'@typescript-eslint/parset'
//语法解析器

ESLint结合自动化工具或者Webpack

结合自动化工具gulp

  1. 在script函数,在babel之前添加插件,处理源代码:

    const script = () => {
        return src('src/assets/script/*.js', {base: 'src'})
        .pipe(plugins.eslint())
        .pipe(plugins.eslint.format())
        .pipe(plugins.eslint.failAfterError())
        .pipe(plugins.babel({presets:[@babel/preset-env]}))
        ...
    }
  2. 完成配置文件初始化:npx eslint

  3. 在eslint配置文件设置:

    globals: {
        '$': 'readonly'
    }
  4. 成功执行gulp任务:npx gulp script


结合webpack,通过loader引入

❄在babel之后、style之前配置:

//webpack.config.js
rules:[
    ...
    {
        test:/\.js$/,
        exclude:/node_modules/,
        enforce:'pre'
    }
    ...
]

报错,react要靠额外插件、定义规则

npm install eslint-plugin-react
//eslintrc.js
rules: [
  'react/jsx-uses-react': 2 ,
  'react/jsx-uses-react': 2 
],//解决react定义没有使用的报错
plugins: [
    'react'
]

基于ESLint的衍生工具

  • Stylelint
  • Prettier
  • Git Hooks

Stylelint工具的使用

  • 提供默认的代码检查规则
  • 提供 CLI 工具,快速调用
  • 通过插件支持 Sass Less PostCSS
  • 支持 Gulp 或 Webpack 集成
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant