一分钟搭建webpack+react的chrome-extensions项目
嘿嘿嘿
客厅吹来的徐徐清风,一缕缕带走房间里沉闷的空气,窗外杨树摇摆着夏日繁茂的枝丫,趴在刚换完床单的床上,耳边“梭梭”声若隐若现,大自然的呢喃远比合成的白噪音悦耳。
周日,又一个阳光且慵懒的下午。
这是发布个人第二款浏览器插件Color Picker的第二天。
温吞吞的节奏中,慢慢学习使用 react 去打造插件应用,最终完成提交的那一刹那,心情却如尘埃落定,没有想象中的兴奋之感。 我果然还是更重视事件的初衷与过程而非结果,确定了方向后,不断的冲刺,无论道路的狭窄崎岖,只管记住目的,去学习,去克服,一次次的冲锋,一次次的受挫,一次次的进步。 这,才是最快乐时候。当到达高潮顶峰之后,只觉得本应如此。
闲言碎语不多讲,来看看基础的项目如何构建。
webpack#
众所周知,一个 webpack 的配置主要由mode(模式)、entry(入口)、output(出口)、loader(转换器)、**plugin(插件)**构成,下面开始手把手搭建一个项目。
init#
# 进入某个专门学习的文件夹mkdir react-app && cd react-app # 创建项目文件夹并进入npm init -y # 快速创建package.json,跳过所有可选项此时一个 package.json 文件已创建好,里面的可选配置后续再说。
在项目根目录下,再创建文件夹config作为 webpack 配置文件的存放,新建文件webpack.config.js,我们要开始配置啦。
# 下载模块yarn add webpack webpack-cli# 个人喜欢使用yarn,也可用npmmode#
mode 一项配置很简单也很重要,作为开发环境的判断,可选择为development或production,并根据不同的环境参数,配置不同的 webpack 优化。
module.exports = { mode: 'production',};webpack 中若无 mode 配置,也可在 package.json 的 scripts 中配置 mode 参数
{ "scripts": { "build": "webpack --config=config/webpack.config.js --mode=production" }}到 package.json 这里就有人看不懂了
”哎?这里还能配置吗?我怎么看不懂啊?这是什么意思啊?“
好看的妹子请加微信求助私人辅导
咳咳,这里我将npm init -y创建的项目默认脚本命令删除了(test: echo 什么的)。
创建了新的命令 名为build,其后的命令为[启动的服务] --[参数名]=[参数值]
我们使用 webpack 的服务,配置其脚本地址(以 package.json 文件为根路径),及 mode 环境。
之后再终端键入npm run build或yarn build即可启动打包命令
这里统一设置为生产环境。
entry#
打包文件的入口,使用path.resolve(__dirname, [path])将文件相对路径转为绝对路径,基本配置如下:
const path = require('path');
module.exports = { entry: path.resolve(__dirname, './index.js'),};由于开发插件的特殊性,需要配置成多入口:
module.exports = { entry: { // 入口文件自定义name: 路径 popup: path.resolve(__dirname, './popup.js'), background: path.resolve(__dirname, './background.js'), contentScript: path.resolve(__dirname, './contentScript.js'), },};output#
入口文件打包后的输出地址
module.exports = { output: { path: path.resolve(__dirname, '../dist'), // 【name】使用entry配置的自定义name作为文件名,这里将输出到/dist/static/js/文件夹下 filename: 'static/js/[name].js', // 根据上条filename的配置,决定了其chunkFilename的名称 // bundle即是打包过程中所使用的模块合并后的产物 chunkFilename: 'static/js/[name].bundle.js', },};module#
module?不是 loader 吗,我这么大一个 loader 去哪了? module 便是用来配置不同类型的 loader。 我们能配置什么呢?
js/jsx#
使用 react,必不可少的就是 jsx 语法和 es678…等等可能会用到的高级语法,所以需要用到 babel 模块将其转换为 es5。
yarn add babel-loader @babel/core @babel/plugin-proposal-class-properties @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react@babel/preset-env指定 babel 可以转化最新的 js 语法,而无需微观管理目标环境所需语法@babel/preset-react使用 jsx 语法必不可少的模块@babel/plugin-proposal-class-properties顾名思义是用于 js class 语法的转换@babel/plugin-transform-runtime为自己的代码创造一个沙盒环境,因为core-js会对全局范围的内置对象进行编译,如:定义与规范不一致的 Promise 对象,core-js 可能会覆盖自己定义的对象,@babel/plugin-transform-runtime 模块就是将内置对象使用模块引入的方式,避免对全局作用域的污染
下载完 babel 模块后,配置规则
module.exports = { module: { rules: [ { test: /\.js(x)?$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'], }, }, ], }, ], },};css 预处理#
根据个人口味选择 loader,这里是 scss
yarn add style-loader css-loader postcss-loader sass-loader node-sassnode-sass 可是太 tm 慢了
module.exports = { module: { rules: [ { test: /\.(sc|c)ss?$/, use: [ 'style-loader', 'css-loader', { loader: 'postcss-loader', options: { plugins: (loader) => [require('postcss-import')({ root: loader.resourcePath })], }, }, 'sass-loader', ], }, ], },};img 图片#
安装url-loader&file-loader,对代码中引用限制大小后的图片,做转 base64 处理,减少请求
module.exports = { module: { rules: [ { test: /\.(gif|png|jpeg|jpg|svg)(\?t=\d+)?$/, use: [ { loader: 'url-loader', options: { outputPath: 'static/images/', limit: 10 * 1024, }, }, ], }, ], },};plugins#
html-webpack-plugin#
此插件可根据入口文件生成一个 html 并引入 js,或将 js 引入到自己配置的模板 html 中。
// 根据多页面的场景,将生成页面单独提出来,这样好看const HtmlWebpackPlugin = require('html-webpack-plugin');const chunkNames = [ { chunk: 'popup', name: '前置弹窗页' }, { chunk: 'background', name: '后置背景页' },];
module.exports = chunkNames.map( ({ chunk, name }) => new HtmlWebpackPlugin({ title: name, filename: `${chunk}.html`, template: `public/index.html`, chunks: ['vendor', chunk], chunksSortMode: 'manual', }),);
// webpack.config.jsconst templateList = require('./moreTemplate');
module.exports = { plugins: [...templateList],};uglifyjs-webpack-plugin#
压缩代码的插件,这没啥说的
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
new UglifyJsPlugin({ test: /\.(js|jsx)/, exclude: /node_modules/, parallel: true, sourceMap: true,}),merge-jsons-webpack-plugin#
由于插件需要做国际化,在各模块中分别加入了其对应的 message.json,使用此插件用于打包合并国际化的 json
// 别问为什么抽出来,好看const locales = ["en","zh_CN","zh_TW"]/** * pattern: 以项目根路径的相对路径,获取json文件拼接 * fileName: 填入output.publicPath的相对路径 */module.exports = locales.map((lang) => ({ pattern: `{./src/_locales/${lang}/messages.json,./src/**/_locales/${lang}/messages.json}`, fileName: `./_locales/${lang}/messages.json`,}));
// webpack.config.jsconst MergeJsonWebpackPlugin = require("merge-jsons-webpack-plugin");const MergeLocale = require("./mergeLocale");new MergeJsonWebpackPlugin({ debug: true, output: { groupBy: [...MergeLocale], },}),optimization#
webpack 中自带可配置的打包可重用模块代码分离优化功能
module.exports = { optimization: { usedExports: true, // tree shaking 只支持import引入 splitChunks: { minChunks: 1, // 需要拆分模块的最小引入次数 chunks: 'all', // 选择分离的区域 cacheGroups: { // 缓存组设置 vendor: { name: 'vendor', // 分离出名为vendor的js,即chunkFilename中的name test: /[\\/]node_modules[\\/]/, priority: -10, }, }, }, },};prod & watch#
根据业务需求,又新建两个文件,watch 开发用,prod 打包用
const merge = require('webpack-merge');const config = require('./webpack.base');
module.exports = merge(config, { watch: true, watchOptions: { aggregateTimeout: 1000, // milliseconds poll: 1000, ignored: ['node_modules'], },});const merge = require('webpack-merge');const { CleanWebpackPlugin } = require('clean-webpack-plugin');const VersionList = require('./syncVersion');const config = require('./webpack.base');
module.exports = merge(config, { plugins: [...VersionList, new CleanWebpackPlugin()],});哎注意细节,这里我又加入一个 versionList,这干嘛的呢? 没有错,是用来同步插件版本号
const version = require('../src/config.json').version;const WriteJsonWebpackPlugin = require('write-json-webpack-plugin');let package = require('../package.json');let manifest = require('../public/manifest.json');
package.version = manifest.version = version;
const list = [ { origin: '../', filename: 'package.json', json: package }, { origin: '../public', filename: 'manifest.json', json: manifest }, { origin: '../dist', filename: 'manifest.json', json: manifest },];
module.exports = list.map( ({ origin, filename, json }) => new WriteJsonWebpackPlugin({ pretty: true, object: json, path: origin, filename, }),);这里抽离了版本号和国际化的所需国际语言数组config方便个人配置。
{ "scripts": { "watch": "webpack --config=config/webpack.watch.js", "build": "webpack --config=config/webpack.prod.js" }}到这里一个不使用 devServer 的 chrome 插件项目的配置就已经完成了,具体的代码文件,请移步到这里欣赏(本身不想开源的,毕竟是个完整的产品,不过为了学习交流还是 open 了) 感觉不错的请不要怜惜我,狠狠地 star 吧!
下章说下插件的开发流程,及我的插件架构思路,嘤嘤