模块化

Posted zhuanglog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模块化相关的知识,希望对你有一定的参考价值。

1、模块化演变过程

立即执行函数

2、commonjs规范

一个文件就是一个模块
每个模块都有点单独的作用域
通过module.exports到处
通过require导入

commonjs是以同步模式加载模块

node没问题但是浏览器段有问题

所以就要使用amd规范,require.js

// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 \'jquery\' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define(\'module1\', [\'jquery\', \'./module2\'], function ($, module2) {
  return {
    start: function () {
      $(\'body\').animate({ margin: \'200px\' })
      module2()
    }
  }
})

require([\'./modules/module1\'], function (module1) {
  module1.start()
})

使用起来较为复杂,但是生态比较好,模块划分过于细致的话js文件请求频繁

3、模块化标准规范

浏览器:ES Modules

node:CommonJS

4、ES Modules 基本特性


  <!-- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 -->
  <script type="module">
    console.log(\'this is es module\')
  </script>


  <!-- 1. ESM 自动采用严格模式,忽略 \'use strict\' -->
  <script type="module">
    console.log(this)
  </script>


  <!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
  <script type="module">
    var foo = 100
    console.log(foo)
  </script>
  <script type="module">
    console.log(foo)
  </script>


  <!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
  <!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->


  <!-- 4. ESM 的 script 标签会延迟执行脚本 -->
  <script defer src="demo.js"></script>
  <p>需要显示的内容</p>

5、ESmodule导出


 var obj = { name, age }


export default { name, age }
//导出一个对象,属性为name,age

export { name, age }


// export name // 错误的用法


// export \'foo\' // 同样错误的用法


setTimeout(function () {
  name = \'ben\'
}, 1000)
//引用方也会在一秒钟之后更改

6、import


// import { name } from \'module.js\'
// import { name } from \'./module.js\'
// import { name } from \'/04-import/module.js\'
// import { name } from \'http://localhost:3000/04-import/module.js\'


// var modulePath = \'./module.js\'
// import { name } from modulePath
// console.log(name)


// if (true) {
//   import { name } from \'./module.js\'
// }

动态导入,直接引用地址
// import(\'./module.js\').then(function (module) {
//   console.log(module)
// })



//命名成员和默认成员都要导入出来
// import { name, age, default as title } from \'./module.js\'
import abc, { name, age } from \'./module.js\'
console.log(name, age, abc)

7、


  <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>

加上nomodule 就可以实现不支持esmodule的浏览器动态加载这个脚本

8、

ES Modules可以导入Common JS模块
CommonJs不能导入ES Modules模块
CommonJS始终导出一个默认成员
注意import不是解构导出对象

9、打包的由来

ES Modules的弊端:

模块化需要处理兼容

模块化的网络请求太频繁,因为每一个文件都要从服务端

所有前端资源除了JS之外都是需要模块化的

打包工具的功能:

编译JS新特性

生产阶段需要打包为一个js文件

支持不同种类的资源类型

10、模块打包工具

webpack:

模块打包器

兼容性处理:loader

代码拆分

资源模块:允许使用js引入其他资源

11、webpack快速上手

12、webpack运行原理

bundle这个文件是一个立即执行函数

传入参数就是代码里的每一个模块

有一个对象来储存加载过的模块,

会按照进入顺序加载模块,

13、资源模块加载

不同的资源文件需要配置不同的loader加载器

在rules里面配置


rules:[
{
  test:/.css$/,
  use:[
    \'style-loader\',
    ‘css-loader\'
  ]
}]


14、webpack导入资源模块

一般还是以js文件为入口

那么其他资源的文件

例如在js文件引入css文件

将整个项目变成了js驱动的项目

15、webpack文件资源加载器

安装file-loader加载器

rules:[
{
  test:/.png$/,
  use:[
    ‘file-loader\',
    ‘css-loader\'
  ]
}]

output:{
   puclicPath:’dist/‘ 根目录文件,这样图片才能找到那个地址
}

16、URL加载器

use:{
  Loader:url-loader,
  options:{
    Limit: 10 * 1024//低于这个大小才用url-loader
  }
}]

小文件用url-loader

17、 js转换

      {
        test: /.js$/,
        use: {
          loader: \'babel-loader\',
          options: {
            presets: [\'@babel/preset-env\']
          }
        }
      },

18、模块加载方式

遵循ES

遵循Common

遵循AMD的define函数和require函数

Loader加载的时候也会触发

样式代码中的@import指令和url函数
@import url(reset.css);

html中的src属性和href属性

  {
    test: /.html$/,
    use: {
      loader: \'html-loader\',
      options: {
        attrs: [\'img:src\', \'a:href\']
      }
    }
  }

19、loader工作原理


markdown-loader

const marked = require(\'marked\')


module.exports = source => {
  // console.log(source)
  // return \'console.log("hello ~")\'这里是因为必须返回js语句

  const html = marked(source)
  // return html
  // return `module.exports = "${html}"` 这样子会导致换行符等被忽略
  // return `export default ${JSON.stringify(html)}` 转成json就可以避免这个问题


  // 返回 html 字符串交给下一个 loader 处理
  return html
}

    rules: [
      {
        test: /.md$/,
        use: [ //顺序是从后往前
          \'html-loader\',
          \'./markdown-loader\'
        ]
      }

20、插件机制

增强自动化能力,例如压缩代码

loader是用来增强资源加载能力的

21、常用插件

自动清理输出目录的插件


  plugins: [
    new webpack.ProgressPlugin(),
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin()
  ]

根据模板动态生成,动态输出


  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\'
    })
  ]

22、开发一个插件

插件比起loader有着更宽泛的能力

本质是钩子机制,webpack打包全流程中会提供钩子


class MyPlugin {
  apply (compiler) {
    console.log(\'MyPlugin 启动\')


    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
          }
        }
      }
    })
  }
}

23、dev server


devServer: {

contentBase: \'./public’, 额外资源路径

    proxy: {
      \'/api\': {//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\': \'\' //正则表达式替换掉api字符
        },
        // 不能使用 localhost:8080 作为请求 GitHub 的主机名
        changeOrigin: true
      }
    }

}
},

24、source map

调试和报错的基础,源代码的地图

//# sourceMappingURL=jquery-3.4.1.min.map

这个注释可以在浏览器当中映射出源代码

25、webpack配置

devtool: \'source-map\',//这个设置可以实现source map

26、webpack eval模式的source map

devtool: \'eval\',

通过eval函数执行,并且在结尾附上那段注释

构建速度是最快的

但是很难定位行列信息,只能定位到文件

27、不同devtool模式对比


module.exports = allModes.map(item => {
    return {
        devtool: item,
        mode: \'none\',
        entry: \'./src/main.js\',
        output: {
            filename: `js/${item}.js`
        },
        module: {
            rules: [
                {
                    test: /\\.js$/,
                    use: {
                        loader: \'babel-loader\',
                        options: {
                            presets: [\'@babel/preset-env\']
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                filename: `${item}.html`
            })
        ]
    }
})
\'eval\',
\'cheap-eval-source-map\', //阉割版的source map,定位时定位在了源代码
\'cheap-module-eval-source-map\',
\'eval-source-map\',
\'cheap-source-map\',
\'cheap-module-source-map\',//阉割版的source map,定位时定位在了
\'inline-cheap-source-map\',
\'inline-cheap-module-source-map\',
\'source-map\',
\'inline-source-map\',
\'hidden-source-map\',
\'nosources-source-map\'

eval-是否使用eval执行模块代码
cheap-是否包含行信息
module-是否能够得到loader处理之前的源代码

inline-把sourcemap嵌入到代码当中
hidden - 没有source map的效果
nosources-能看到错误信息,但是浏览器上面看不到

如何选择呢?

选择合适的source map

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

原因:

代码每行不超过80个字符
经过loader转换后变化较大
首次打包速度慢无所谓,重新打包较快

生产模式中:none

source map会暴露源代码

28、自动刷新

自动刷新会导致页面状态丢失

页面不刷新

29、HMR(模块热更新)

在运行过程的即时替换,应用运行状态不受影响


  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
  devServer: {
    hot: true
    // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
  },



// ============ 以下用于处理 HMR,与业务代码无关 ============
// main.js

if (module.hot) {先判断是否存在这个模块
  let lastEditor = editor
  module.hot.accept(\'./editor\', () => {
    // console.log(\'editor 模块更新了,需要这里手动处理热替换逻辑\')
    // console.log(createEditor)


    const value = lastEditor.innerHTML
    document.body.removeChild(lastEditor)
    const newEditor = createEditor()
    newEditor.innerHTML = value
    document.body.appendChild(newEditor)
    lastEditor = newEditor
  })


  module.hot.accept(\'./better.png\', () => {
    img.src = background
    console.log(background)
  })
}

HMR的特殊逻辑会在编译之后被清掉

30、生产环境优化

mode的用法

不同环境下的配置

①、配置文件根据环境不同导出不同配置


module.exports = (env, argv) => {
  const config = {
    mode: \'development\',
    entry: \'./src/main.js\',
    output: {
      filename: \'js/bundle.js\'
    },
    devtool: \'cheap-eval-module-source-map\',
    devServer: {
      hot: true,
      contentBase: \'public\'
    },
    module: {
      rules: [
        {
          test: /\\.css$/,
          use: [
            \'style-loader\',
            \'css-loader\'
          ]
        },
        {
          test: /\\.(png|jpe?g|gif)$/,
          use: {
            loader: \'file-loader\',
            options: {
              outputPath: \'img\',
              name: \'[name].[ext]\'
            }
          }
        }
      ]
    },
    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
}
    

②、一个环境对应一个配置文件


const webpack = require(\'webpack\')
const merge = require(\'webpack-merge\') //可以满足合并配置的功能,不会替换掉公有里面的同名属性
const common = require(\'./webpack.common\')


module.exports = merge(common, {
  mode: \'development\',
  devtool: \'cheap-eval-module-source-map\',
  devServer: {
    hot: true,
    contentBase: \'public\'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
})

31、DefinePlugin

为代码注入


plugins: [

new webpack.DefinePlugin({

// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify(\'https://api.example.com\')

})
]

32、treeShaking


optimization: {

// 模块只导出被使用的成员,标记枯树枝
usedExports: true,

// 尽可能合并每一个模块到一个函数中,作用域提升
concatenateModules: true,

// 压缩输出结果,把树枝要下来
// minimize: true
}

生产环境中会自动开启

33、Tree-shaking & Babel

必须使用ES Modules

babel-loader


module: {

rules: [

{
test: /\\.js$/,

use: {

loader: \'babel-loader\',

options: {

    presets: [

      // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
      // [\'@babel/preset-env\', { modules: \'commonjs\' }]
      // [\'@babel/preset-env\', { modules: false }]
      // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
      // 设置为commonjs会强制转换
          [\'@babel/preset-env\', { modules: \'auto\' }]

       ]
     }
}
}
]
},
optimization: {

// 模块只导出被使用的成员
usedExports: true,

// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true
}

34、sideEffects

optimization: {

sideEffects: true,

}

如果一个模块里被引用进来但是只用里面的一个模块,那么其它模块就不会被引入

import { Button } from \'./components\'
//components当中除了Button之外的模块都不会被引入

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

// 副作用模块
import \'./extend\'

在extend当中为number原型添加了一个方法,这样就会导致这个方法是没有被引入的

解决办法

"sideEffects": [
"./src/extend.js",
"*.css"
]

在package.json文件中

35、代码分割

并不是每个模块都是在启动时需要的

所以需要实现按需加载

但是也不能分的太细

同域并行请求

请求头占资源

两种解决方法

①多入口打包


entry: {

index: \'./src/index.js\',

album: \'./src/album.js\'

},注意这里是对象,数组的话就是多个文件打包到一个文件

output: {

filename: \'[name].bundle.js’//编译结束后会替换[name]

},







plugins: [

new CleanWebpackPlugin(),

new HtmlWebpackPlugin({

title: \'Multi Entry\',

template: \'./src/index.html\',

filename: \'index.html\',

chunks: [\'index\']

}),
new HtmlWebpackPlugin({

title: \'Multi Entry\',

template: \'./src/album.html\',

filename: \'album.html\',

chunks: [\'album’] //指定加载那个打包文件

})
]

提取公共模块


  optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: \'all\'
    }
  },

②动态导入

按需加载



const render = () => {
  const hash = window.location.hash || \'#posts\'


  const mainElement = document.querySelector(\'.main\')


  mainElement.innerHTML = \'\'


  if (hash === \'#posts\') {
    // mainElement.appendChild(posts())
    import(/* webpackChunkName: \'components\' */\'./posts/posts\').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === \'#album\') {
    // mainElement.appendChild(album())
    import(/* webpackChunkName: \'components\' */\'./album/album\').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}


render()


window.addEventListener(\'hashchange\', render)

36、minicss

  module: {
    rules: [
      {
        test: /\\.css$/,
        use: [
          // \'style-loader\', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          \'css-loader\'
        ]
      }
    ]
  },

37、OptimizeCssAssetsWebpackPlugin

  optimization: {
    //配置在这里可以保证只有压缩功能开启时才会执行下面插件
    //所以需要自己手动保证一下js的压缩
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin() 压缩样式文件
    ]
  },

38、输出文件名hash

  output: {
    filename: \'[name]-[contenthash:8].bundle.js\'
  },

通过hash值控制缓存

以上是关于模块化的主要内容,如果未能解决你的问题,请参考以下文章

如何将字符串数据从活动发送到片段?

CTS测试CtsWindowManagerDeviceTestCases模块的testShowWhenLockedImeActivityAndShowSoftInput测试fail项解决方法(代码片段

argparse 代码片段只打印部分日志

nodejs常用代码片段

常用python日期日志获取内容循环的代码片段

使用循环片段依赖关系模块化单活动Android应用程序