7.模块化开发笔记(主要webpack)

本文为拉勾网大前端高薪训练营第一期心得和笔记


学习心得

前端学习背景

经过这段时间拉勾网大前端高薪训练营的学习,进步还是挺明显的,主要是前端这个行业细节太丰富,经常是用到什么查什么,并没法系统地学习前端,自己琢磨会遗漏很多细节,还有很多小技巧和好用的工具也是会错过的。

课程和视频

拉勾网的大前端训练营的视频讲解很详细,视频分成很多小节,每个视频平均5分钟左右,少数复杂的15分钟,比较方便碎片时间学习,当然还是比较推荐边学边记笔记,不然学完就忘了。学习计划也是循序渐进,不过还是需要有些前端基础学习效果最好。视频的代码也会在代码库里更新,节省了自己敲打的时间,视频也是2年有效,随时可以回来翻看。每一个小章节后有问卷作业,每一个大章节后有一次问答题和代码题的作业,每一个大章节之后会有一次串讲的直播,赶不上可以看录播,总的来说学习体验还是挺好的。

班主任

班主任很尽责,会通知大家直播时间(催促大家写作业,笑),学起来挺有紧迫感的(杜绝了steam买游戏不玩的,买了教程不学的情况),另外有两个微信群(摸鱼和学习),班主任经常在摸鱼群里发励志文章和表情包,鼓励大家努力学习:D。

答疑和导师

有什么疑问可以问老师和同学,包括讲课的老师和批改作业的老师都在微信群里,很方便。
答疑不仅是课程里,也可以是工作学习中遇到的问题,这相当于还增进了人脉,一举两得。说不准学完了得到个内推什么的,那就更赚了。


笔记

script type=‘module’

自动严格模式

单独私有作用域

通过CORS的方式请求外部JS模块,且必须http这种形式,不能通过文件

等同于defer,延迟执行脚本

import export

es module import和export都可以用as重命名

import和export是传引用(变量地址),而不是传值

import的不能改,是const的

import a from 'a.js’不能省略.js

import b from ‘./b/index.js’ 不能省略index.js

import 'modules.js’会认为是外部模块

'./modules.js‘ 
'/04-import/modules.js' 
'http://localhost:3001/04-import/modules.js'
等效

动态加载模块

import(importPath).then((module)=>{
	console.log(module)
})

script nomodule,只有在不支持module的浏览器才运行脚本,可以用来阻止Polyfill在支持module的浏览器里执行第二次脚本

nodejs ES module

nodejs支持module,需要后缀改成mjs,执行时

node --experimental-modules index.mjs

ESModule这种方式,系统api支持import {writeFileSync} from ‘fs’,lodash这种第三方的不支持这样使用

  • esm可以导入commjs模块
  • commonjs只能导出一个默认成员
  • import不是解构导出对象
  • commonjs不能导入ESM模块

ESM里不能使用require module exports __filename __dirname这些commonjs内置的模块

替代做法

// 通过 url 模块的 fileURLToPath 方法转换为路径
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)

type module

node 12.10版本可以在package.json里增加"type": “module”

此时commonjs的文件需要改成cjs后缀

babel转换

babel-node index.js --presets=@babel/preset-env
//或者创建.babelrc
{
	"presets": ["@babel/preset-env"]
}

//或者选择只用某个插件
{
	"plugins": [
		"@babel/plugin-transform-modules-commonjs"
	]
}

webpack

好文章收藏

https://segmentfault.com/a/1190000019890322

https://blog.csdn.net/qq_36380426/article/details/106894870

初步认识

webpack4 零配置的话会打包src/index.js到dist/main.js

三种mode production development none

小技巧vs code: cmd+k cmd+0折叠所有代码

rules里的use里的loader执行顺序是从后向前

config里的publicPath告诉webpack文件路径

config里limit可以指定文件大小是否使用loader limit: 10*1024就是10KB

资源加载支持

  • ES Module标准的import声明
  • CommonJS标准的require函数
  • AMD标准的的define函数和require函数
  • 样式代码中的@import指令和url函数 @import url(test.css);
  • html里图片标签的src,想增加的话可以如下
{
	test: /.html$/,
	use: {
		loader: 'html-loader'
		options: {
			attrs: ['img:src', 'a:href']
		}
	}
}

自己写loader

const marked = require('marked')

module.exports = source => {
  // console.log(source)
  // return 'console.log("hello ~")'
  const html = marked(source)
  // return html
  // return `module.exports = "${html}"`
  // return `export default ${JSON.stringify(html)}`

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

返回导出的字符串,然后webpack会执行拼接进js代码里

webpack plugins

clean-webpack-plugin 自动删除生成的文件

html-webpack-plugin 生成一个使用bundle.js的html

new HtmlWebpackPlugin({
  title: 'Webpack Plugin Sample',
  meta: {
    viewport: 'width=device-width'
  },
  template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
  filename: 'about.html'
})

copy-webpack-plugin

new CopyWebpackPlugin([
  // 'public/**'
  'public'
])

手写插件

这个例子是删除bundle.js里所有的注释,

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

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

自动修改更新,并刷新浏览器

方法1 不推荐

webpack —watch

browser-sync dist —files “**/*”

方法2

webpack-dev-server —open

这个不会写磁盘,打包好的是存在内存里,开发会快

只有webpack打包的东西才能被dev-server访问到

webpack config,加了这个dev-server就能访问到了

devServer: {
    contentBase: './public',
}

webpack-dev-server proxy

proxy: {
  '/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 sourcemap

'eval',
代码在eval里执行,只能看到哪个文件,不能看到行列
'cheap-eval-source-map',
代码在eval里执行,只能看到行,没有列,代码是es6转换成es5的
'cheap-module-eval-source-map',
代码在eval里执行,只能看到行,没有列,代码是原本一样的,没有经过loader加工
'eval-source-map',
代码在eval执行,能看到错误的行,列
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
source map是用inline dataurl的方式存在文件里,而不是单独.map文件
'hidden-source-map',
在js里不引用source map,但是生成了source map
'nosources-source-map'
没有源代码,但是提供错误的行列信息

规律就是

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

开发环境sourcemap推荐cheap-module-source-map

理由

  1. 每行代码不超过80字符,没有列信息也能定位错误
  2. loader编译react和vue的代码差异很大,需要源代码
  3. 首次打包速度慢无所谓,重写打包相对较快

生产环境sourcemap:none

如果实在要在生产环境用sourcemap,用nosources-source-map,只会报错的行列,不会暴露源代码

webpack HMR(Hot Module Replacement)模块热拔插

webpack-dev-server —hot

或者config里开启

const webpack = require('webpack')
devServer: {
	hot: true
},
plugins: [
	new webpack.HotModuleReplacementPlugin()
]

HMR对于css可以开箱即用,但是js不行,因为js文件导出没有规律,需要自己制定HMR API

//手动写热更新的API
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问题

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

HMR API代码有错误,更新代码后会自动刷新导致看不到报错信息

解决方法:hotOnly: true

  1. 没启动HMR的情况下,HMR API报错

判断是否存在module.hot,再执行

3.代码中多了一些与业务无关的代码,不会打包到最后的代码里

merge prod和common config

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 prod会默认开启DefinePlugin,注入全局变量process.env.NODE_ENV

手动注入的时候注意,会替换这个变量为你写的字符串,所以需要提前JSON.stringify()

prod会自动开启tree-shaking,dead code比如return后的代码,和未使用到的函数等代码都不会打包到最后js里

手动开启tree shaking

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    // 模块只导出被使用的成员,相当于标记枯树枝
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中,scope hoisting作用域提升
    concatenateModules: true,
    // 压缩输出结果,删除未使用的,摇掉枯树枝
    // minimize: true
  }
}

babel-loader默认保留ESM,因此tree shaking还能正常工作,tree shaking必须要基于ESM模块才行,如果要强行commonjs模块,配置是是数组套数组

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 转换
          ['@babel/preset-env', { modules: 'auto' }]
        ]
      }
    }
  }
]

prod会自动开启sideEffects

sideEffects用来标记不用的代码删除是否有副作用,需要在告诉webpack,没有用到的模块不会打包

手动开启的时候会做两个事情,

optimization: {sideEffects: true}

package.json里增加sideEffects: false,

挂载在prototype上的method,还有css都算是sideEffects,要么彻底关闭,要么告诉webpack哪些文件是有副作用的,不要删除这些里面的代码,做法是在package.json里

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

代码分割

两种方式

  • 多入口
  • 动态导入

多入口entry需要是{},如果是[]的话就是多文件打包在一起

注意output和HtmlWebpackPlugin的配置chunks

entry: {
  index: './src/index.js',
  album: './src/album.js'
},
output: {
  filename: '[name].bundle.js'
},

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

自动提取公共模块到单独bundle

optimization: {
  splitChunks: {
    // 自动提取所有公共模块到单独 bundle
    chunks: 'all'
  }
},
//比如例子里会生成album-index.bundle.js

动态加载

在js代码里用esm的import then的方式来加载模块

如果是react或vue的话,建议在路由部分动态加载import then

一般这样生成的文件是1.bundle.js, 2.bundle.js

如果要给文件起名,就如下webpackChunkName: ‘name’ 魔法注释,告诉webpack文件名

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

实现css按需加载

建议超过150KB的话才按需加载css,注意不要使用style-loader,这是把css inline到js的loader

OptimizeCssAssetsWebpackPlugin压缩css,这个插件可以放在Plugins里,但是更推荐放在optimization的minimizer里,这样dev时就不开启,prod就开启

有个副作用,就是如果配置了optimization minimizer,默认的js压缩器就被覆盖了,所以还得手动写进去TerserWebpackPlugin用来压缩js文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

optimization: {
  minimizer: [
    new TerserWebpackPlugin(),
    new OptimizeCssAssetsWebpackPlugin()
  ]
},
module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        // 'style-loader', // 将样式通过 style 标签注入
        MiniCssExtractPlugin.loader,
        'css-loader'
      ]
    }
  ]
},
plugins: [
  new CleanWebpackPlugin(),
  new HtmlWebpackPlugin({
    title: 'Dynamic import',
    template: './src/index.html',
    filename: 'index.html'
  }),
  new MiniCssExtractPlugin()
]

hash文件名

用来解决浏览器缓存过长无法更新文件,过短缓存效果不好的问题

凡是文件名的地方都支持占位符来实现hash

三种方式

[hash] 代码任何地方改动,都会引起所有的文件名变化

[chunkhash] chunk代码改动才会引起同一个chunk的文件名变化

[contenthash] 文件级别的hash,只会根据当前文件生成hash

[contenthash:8]可以控制hash数字位数

控制缓存推荐contenthash:8

output: {
  filename: '[name]-[hash].bundle.js'
},

plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import',
      template: './src/index.html',
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name]-[contenthash:8].bundle.css'
    })
  ]

rollup高效的ESM打包器

自动会开启tree-shaking

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

rollup使用配置文件

yarn rollup --config

rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'iife'
  }
}

使用插件

webpack三种扩展方式loader plugin minimizer

rollup只有plugin

import json from 'rollup-plugin-json'
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'iife'
  },
  plugins: [
    json()
  ]
}

rollup不能直接像webpack一样直接import名称导入nodemodules的模块

需要只用rollup-plugin-node-resolve

rollup默认只能处理es版本的模块,比如lodash-es,如果要使用普通的模块,需要roll-plugin-commonjs

rollup多入口可以是input写[‘foo.js’, ‘bar.js’],也可以是{foo: ‘foo.js’, bar: ‘bar.js’}

rollup代码拆分,动态导入import(’’).then

代码拆分必须使用amd或者commonjs,不能用umd或者IIFE,浏览器只能用amd

如果html要加载amd代码不能直接引用,需要requirejs


format: amd umd commjs iife

如果是开发一个类库/框架,就可以使用rollup,如果是开发webapp,webpack更好,因为引用第三方库,HMR都很好用

Parcel零配置的前端应用打包器

yarn add parcel-bundler —dev

yarn parcel src/index.html

会打包并自动server运行html,支持模块热替换

parcel hmr api只有一个参数

if (module.hot) {
  module.hot.accept(() => {
    console.log('hmr')
  })
}

parcel自动安装库的依赖

比如开发过程中加入jquery,不用停止server

也支持拆分代码,用动态引入, import then

parcel prod

yarn parcel build src/index.html

parcel打包速度比webpack快,因为使用多进程同时工作,发挥多核cpu性能

webpack也可以用happypack的插件来实现多进程工作

ESLint

yarn add eslint --dev
yarn eslint --init
//popular format: airbnb standard google;其中standard不需要后面加分号
yarn eslint ./index.js
yarn eslint ./index.js --fix //自动修复

env列表

  • browser - browser global variables.
  • node - Node.js global variables and Node.js scoping.
  • commonjs - CommonJS global variables and CommonJS scoping (use this for browser-only code that uses Browserify/WebPack).
  • shared-node-browser - Globals common to both Node.js and Browser.
  • es6 - enable all ECMAScript 6 features except for modules (this automatically sets the ecmaVersion parser option to 6).
  • es2017 - adds all ECMAScript 2017 globals and automatically sets the ecmaVersion parser option to 8.
  • es2020 - adds all ECMAScript 2020 globals and automatically sets the ecmaVersion parser option to 11.
  • worker - web workers global variables.
  • amd - defines require() and define() as global variables as per the amd spec.
  • mocha - adds all of the Mocha testing global variables.
  • jasmine - adds all of the Jasmine testing global variables for version 1.3 and 2.0.
  • jest - Jest global variables.
  • phantomjs - PhantomJS global variables.
  • protractor - Protractor global variables.
  • qunit - QUnit global variables.
  • jquery - jQuery global variables.
  • prototypejs - Prototype.js global variables.
  • shelljs - ShellJS global variables.
  • meteor - Meteor global variables.
  • mongo - MongoDB global variables.
  • applescript - AppleScript global variables.
  • nashorn - Java 8 Nashorn global variables.
  • serviceworker - Service Worker global variables.
  • atomtest - Atom test helper globals.
  • embertest - Ember test helper globals.
  • webextensions - WebExtensions globals.
  • greasemonkey - GreaseMonkey globals.

.eslintrc例子

module.exports = {
  env: {
    browser: false,
    es6: false
  },
  extends: [
    'standard'
  ],
  parserOptions: {
    ecmaVersion: 2015
  },
  rules: {
    'no-alert': "error"
  },
  globals: {
    "jQuery": "readonly"
  }
}

用注释禁用某一行eslint检查

 const str1 = "${name} is a coder" // eslint-disable-line no-template-curly-in-string

gulp eslint

const script = () => {
	return src('src/assets/scripts/*.js', {base: 'src'})
		.pipe(plugins.eslint())
		.pipe(plugins.eslint.format()) //打印错误
		.pipe(plugins.eslint.failAfterError()) //有错误停止执行
		.pipe(plugins.babel({presets: ['@babel/preset-env']}))
		.pipe(dest('temp'))
		.pipe(bs.reload({stream: true}))
}

webpack eslint

{
	test: /\.js$/,
	exclude: /node_modules/,
	use: 'eslint-loader',
	enforce: 'pre  //让这个rule提前执行
}

react项目的eslint

yarn add eslint-plugin-react --dev

rules: [
	'react/jsx-uses-react': 2, //2 === error
	'react/jsx-uses-vars': 2
],
plugins: [
	'react'
]

typescript eslint

//.eslintrc.js
yarn eslint --init
//选择typescript yes
parser: '@typescript-eslint/parser'
plugins: ['@typescript-eslint']

stylelint

yarn add stylelint --dev
//.stylelintrc.js
module.exports = [
	extends: [
		'stylelint-config-standard',
		'stylelint-config-sass-guidelines'
	]
]
//yarn add stylelint-config-standard --dev
yarn stylelint ./index.css

yarn add stylelint-config-sass-guidelines --dev

prettier

yarn add prettier --dev
yarn prettier style.css --write//不加--write会打印出来
yarn prettier . --write//把根目录的文件都改了

git hooks husky lint-staged

https://zhuanlan.zhihu.com/p/27094880

yarn add husky --dev
yarn add lint-staged --dev

//package.json
"scripts": {
	"test": "eslint ./index.js",
	"precommit": "lint-staged"
},
"husky": {
	"hooks": {
		"pre-commit": "npm run precommit"
	}
},
"lint-staged": {
	"*.js": [
		"eslint",
		"git add"
	]
}

你可能感兴趣的:(前端,webpack)