模块化开发与规范化标准

324 阅读20分钟

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

模块化演变过程

早期:

  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')
            //指定输出文件所在的目录
            //都需要使用绝对路径
        }
    }
    

    github.com/jawil/blog/…

    • __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:以代码的形式直接表示一个文件

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

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

使用:

  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就这样完成项目的模块化

核心工作原理

  • 依赖树

    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支持配置代理

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

      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'
    //根据输出文件内容输出哈希值,不同的文件就有不同的哈希值,最适合解决缓存问题
    

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'

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

我们看下 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 集成

本文首发于我的GitHub博客,其它博客同步更新。