目录文件
## command 命令文件
## lib -- 转义生成的代码
## node_modules
## src -- 源代码
## typings -- 类型声明文件
## .gitignore
## index.js -- 入口文件
## package.json
## tsconfig.json -- ts配置文件
## readme.md
命令步骤
分解(parsing) -> 转译(babel) -> 压缩(minfiles)
React转化为ES5代码(包含所有ES6转化为ES5代码)
核心是利用babel的一组预设 @babel/preset-react 来对react进行转化,随后再使用 @babel/env 来进行ES6 - ES5的转化
1.npm包内安装babel相关依赖
{
"devDependencies": {
"@babel/preset-react": "^7.13.13",
"@babel/cli": "^7.13.14",
"@babel/core": "^7.13.15",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.13.15",
"react": "^17.0.2",
"react-dom": "^17.0.2",
},
}
2. 配置bebel文件(babel7 -> babel.config.json)
babel文件的读取顺序是由下往上的
{
"presets": [
[
"@babel/env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
],
[ "@babel/preset-react" ]
]
}
3. 使用babel命令进行转译
./node_modules/.bin/babel src --copy-files --out-dir lib --presets=@babel/env,@babel/preset-react
src - 转译src目录内的文件 –out-dir - 表示编译的类型是文件 --copy-files - 拷贝一份不进行编译的文件到目标文件夹 --presets=@babel/env - 添加预设(@babel/env,@babel/preset-react)
注意项
暂无。
这样就完成了对React文件和所有ES6的转化!
参考链接: babel官网 如何使用ES6編写一个 React模块,并且编译后发布到NPM
利用fs + UglifyJS完成对 .js 文件的压缩与混淆
核心就是利用fs模块获取文件列表,递归获取文件信息并存储,然后在原来的架子上写入 .min.js 文件,最后再将 .js 文件删除
- 原来的架子是指在进行babel转化的时候已经将目录搭好,我们只需要将新文件按原路径写入即可,省去了自己主动去创建文件夹的操作
废话不多说,直接上完整代码,搭配注释便能理解。
let fs = require('fs');
let UglifyJS = require('uglify-js');
// .css 文件手动压缩
function iGetInnerText(testStr) {
var resultStr = testStr.replace(/\ +/g, ""); //去掉空格
resultStr = testStr.replace(/[ ]/g, ""); //去掉空格
resultStr = testStr.replace(/[\r\n]/g, ""); //去掉回车换行
return resultStr;
}
// 递归对每个文件进行写入
function writefs(obj, toPath, pPath = toPath) {
for (let i in obj) {
if (!fs.lstatSync(`${pPath + '/'}${i}`).isDirectory()) {
let tPath = pPath.replace(toPath, toPath)
fs.writeFile(`${tPath}/${obj[i].toFileName}`, obj[i].code, 'utf-8', function (err) {
if (err) throw err;
console.log('success');
if (i.indexOf('.js') > 0 || i.indexOf('.less') > 0) {
// 写入完成删除源文件
fs.unlinkSync(`${tPath}/${i}`)
}
})
} else writefs(obj[i], toPath, `${pPath + '/'}${i}`)
}
}
// 递归拿到所有文件,并重命名、获取文件信息
function getAllFiles(pathTo, obj = {}) {
// 读取当前文件夹
let nowLevelFiles = fs.readdirSync(pathTo)
nowLevelFiles.forEach(i => {
// 判断是否是文件夹
if (!fs.lstatSync(`${pathTo}/${i}`).isDirectory()) {
let newI = i.replace('.js', '.min.js')
// 拿到文件内容
let fileContent = fs.readFileSync(`${pathTo}/${i}`, 'utf-8'), fileType = i.split('.')[1]
obj[i] = {
form: `${pathTo}/${i}`,
toFileName: newI,
// 如果是 .js 文件,利用 UglifyJS 进行压缩,混淆,如果不是则利用正则消除空格
code: fileType === 'js' ? UglifyJS.minify({ [i]: fileContent }).code : iGetInnerText(fileContent),
}
} else obj[i] = getAllFiles(`${pathTo}/${i}`, {})
})
return obj
}
let Uglify = function (toPath) {
writefs(getAllFiles(toPath), toPath);
}
Uglify('./lib');
21-4-21
优化了一下 uglify.js ,将命令文件归拢一处,更加简洁
新增getFiles.js
let fs = require('fs');
module.exports = function getFiles(path) {
let files = []
getAllFiles(path)
// 递归拿到所有文件,并重命名、获取文件信息
function getAllFiles(path) {
// 读取当前文件夹
let nowLevelFiles = fs.readdirSync(path)
nowLevelFiles.forEach(i => {
// 判断是否是文件夹
if (!fs.lstatSync(`${path}/${i}`).isDirectory()) {
files.push({fileName: i, path: `${path}/${i}`, parentPath: path})
} else getAllFiles(`${path}/${i}`, {})
})
}
return files
}
优化 uglify.js 简单来说就是将递归获取所有文件的步骤摘了出去,作为一个功能函数来使用,更加符合 “美感”(?)。然后就是多了一个筛选,可以选择不进行压缩的文件。
let fs = require('fs');
let UglifyJS = require('uglify-js');
let getFiles = require('./getFiles')
const noCompress = ['JSBriged.js'] // 不需要压缩的文件名称
// .css 文件手动压缩
function iGetInnerText(testStr) {
var resultStr = testStr.replace(/\ +/g, ""); //去掉空格
resultStr = testStr.replace(/[ ]/g, ""); //去掉空格
resultStr = testStr.replace(/[\r\n]/g, ""); //去掉回车换行
return resultStr;
}
// 对每个文件进行写入
function writefs(obj, toPath, pPath = toPath) {
let allFiles = getFiles(toPath)
for (let i in obj) {
fs.writeFile(obj[i].newPath, obj[i].code, 'utf-8', function (err) {
if (err) throw err;
console.log('success');
if (i.indexOf('.js') > 0 || i.indexOf('.less') > 0) {
// 写入完成删除源文件
fs.unlinkSync(obj[i].form)
}
})
}
}
// 拿到所有文件,并重命名、获取文件信息
function setFiles(pathTo) {
let allFiles = getFiles(pathTo)
let obj = {}
allFiles.map((item, index) => {
// 排除
if (noCompress.indexOf(item.fileName) !== -1) return
let newI = item.fileName.replace('.js', '.min.js')
// 拿到文件内容
let fileContent = fs.readFileSync(item.path, 'utf-8'), fileType = item.fileName.split('.')[1]
obj[item.fileName] = {
form: item.path,
toFileName: newI,
newPath: `${item.parentPath}/${newI}`,
// 如果是 .js 文件,利用 UglifyJS 进行压缩,混淆,如果不是则利用正则消除空格
code: fileType === 'js' ? UglifyJS.minify({ [item.fileName]: fileContent }).code : iGetInnerText(fileContent),
}
})
return obj
}
let Uglify = function (toPath) {
writefs(setFiles(toPath), toPath);
}
Uglify('./lib');
21-5-31 -- 按需加载
前几天实现了按需加载,今天记录一下实现方式
step1: 更改所有文件内的导出方式
递归导入所有模块。由于第二步需要利用fs进行操作,所以导出需要node识别,也就是使用commonJS导入导出 (module.exports = {导出的模块})
step2: 分离(parsing)
所有模块引入之后,利用 递归 + fs模块 将每个方法(fn)循环生成到文件夹(原文件名)内部,每个方法额外拼接 export default,中途记录每个文件的名称及路径,在生成完毕之后动态生成 index.js ,利用模版字符串动态将每个文件以ES6形式导入导出
由于react组件的特殊性(.jsx 内部引入react(主要原因) ) 不参与分割,本身进行开发的时候就以模块形式开发,不聚一起。 所以在这里直接跳过分割,只进行文件拷贝(copyFiles)
// parsing.js
const fs = require('fs');
const path = require('path')
const getFiles = require('./getFiles')
const copyFiles = require('./copyFiles')
/*
导入主文件
获取所有导出值
利用fs遍历生成新文件
新建一个外部index,将所有遍历生成的文件引入其中 动态模版生成
*/
// index.js 到目标 文件的路径
class Parsing {
// originPath - 源路径 targetPath - 目标路径 mainPath - 入口文件路径
constructor(originPath, targetPath, mainPath) {
this.path = originPath
this.fileDetail = {} // 文件路径数据存储
this.storePath = './lib/' // 最终文件存储路径
this.resolvePath = targetPath
this.indexPath = mainPath
}
init() {
try {
fs.mkdirSync(this.resolvePath)
} catch { }
// react component 拷贝一份到目标文件夹,不参与分割
copyFiles(this.path, this.resolvePath, 'ReactComponents')
// 拿到所有目标目录及自文件路径集合
let files = getFiles(this.path)
// 排除 不是 .js 后缀形式文件 排除 react component 另外处理
files = files.filter(item => {
let parentN = item.parentPath.split('/')
return path.extname(item.fileName) === '.js' && parentN[parentN.length - 1] !== 'ReactComponents'
})
// 拿到当前文件夹下的所有文件 目前仅一层,若优化使用递归
for (let file of files) {
// 获取文件内的所有导出
let fileExportFnList = require(file.path)
let dirName = file.fileName.split('.')[0]
// 写入文件夹
try {
fs.mkdirSync(`${this.resolvePath}/${dirName}`)
} catch { }
// 遍历生成新文件
this.mkFile(fileExportFnList, dirName)
}
this.createIndex()
}
// 遍历生成新文件
mkFile(fileExportFnList, dirName) {
let dirDatas = []
for (let i in fileExportFnList) {
let exportFn = fileExportFnList[i]
dirDatas.push({
path: `${this.storePath}${dirName}/${i}.js`,
name: i
})
let filePath = `${this.resolvePath}/${dirName}/${i}.js`
fs.writeFileSync(filePath, 'export default ' + exportFn, 'utf-8')
}
// 记录每一个文件内所有导出模块(新文件)的路径及模块名称
this.fileDetail[dirName] = dirDatas
}
//自动生成index.js
createIndex() {
let contentData = ''
let exportCon = ''
let fileD = this.fileDetail
let RCFiles = getFiles(`${this.resolvePath}/ReactComponents`)
// 筛选出后缀仅为 .jsx 的文件 且对后缀进行替换并将格式与 fileDetail 同步
RCFiles = RCFiles.filter(i => path.extname(i.fileName) === '.jsx').map(j => {
return {
path: `${this.storePath}${j.path.split('separate')[1].replace('.jsx', '.js')}`,
name: j.fileName.split('.jsx')[0]
}
})
fileD = { ...fileD, 'ReactComponents': RCFiles }
let dirNames = ''
// 合并统一处理入口文件的导入
for (let dir in fileD) {
let names = ''
dirNames += dir + ', '
let files = fileD[dir]
for (let i = 0; i < files.length; i++) {
let { name, path } = files[i]
names += name + ', '
contentData +=
`
import ${name} from '${path}'
`
}
// 版本向下兼容,将一个文件中的所有模块包裹一起,键名为文件名称
contentData +=
`
let ${dir} = { ${names.slice(0, names.length - 2)} }
`
exportCon += names
}
// 删除额外 , 号
exportCon = exportCon.slice(0, exportCon.length - 2)
dirNames = dirNames.slice(0, dirNames.length - 2)
let finallyData =
`
${contentData}
export { ${exportCon}, ${dirNames} }
`
// 合并后写入
fs.writeFileSync(this.indexPath, finallyData, 'utf-8')
}
}
const parsing = new Parsing(
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../separate'),
path.resolve(__dirname, '../index.js')
)
parsing.init()
step3 babel转化
这个点主要注意的是在babel转化时需要将转化模块更改为 ES6导入导出(默认commonJS)
babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"modules": false // 更改为 ES6导入导出
}
],
["@babel/preset-react"]
]
}
step4 uglify压缩
和之前一样,不贴代码了
小结
之所以将第一、第三步分开,主要还是node不识别ES6导入导出,所以只能自己以cmooonJS形式去导出,然后在拆分时再前设 export default,这样便符合了 treeshaking 的前置要求
22-4-8 ts转化 - 1.5.0
这次更新主要是想对ts下手,其他的都是临时加上去的,只不过到后来反而是重构命令文件占了大头,因为.jsx和.js 转化为 .tsx 和 .ts 之后,分离(parsing.js)步骤也要进行对应的更改,昨天着实让我头痛了一下处理方式,不过好在最后都找到了解决方式HAHA.
1 使用styled-component之后babel时产生的异常
这个样子: 在一个 .tsx 组件中使用styled-component插件,然后tsx转译成commonJS的ES5(target)格式内容,在项目中使用的时候报错,显示 React is not defined,查看问题之后发现是styled-component生成的div被识别成React组件
最后在找资料(baidu)的时候,发现babel有对此插件的处理:在babel配置文件中添加plugins: "babel-plugin-styled-components"
然后用babel编译即可
其实我在发现tsc可以直接转译es5内容时,我是想要放弃babel的-。= 还好没有手快吧babel的处理删掉,不然直接裂开
2.tsconfig.json无法重用
我之前是吧 devtools.js和 regModules.js 手动改成 module.exports
的导出方式,然后再有parsing.js分离时手动改为 export default
模式,然后ReactComponents内的内容不参与分离,就会在分离后与被分离的文件们同步导出方式。
但是现在由于 tsc 是统一去处理模块代码的,就出现了一个很尴尬的情况:当我在tsconfig.json中配置module为none
时,devtools和regModules的内容时可以正常编译的,而ReactComponent的内容则被编译成了module.exports方式,然后react组件无法参与组件,就会导致babel时报错,无法引入..
当时我看到这个问题人直接就麻了,最开始想要分别搞两个配置文件去处理不同情况,但是这样的话命令归拢就行不通了,而且在生成最终文件时会异常麻烦,这显然是不符合预期的,所以在我翻了好多资料之后,突然想到,我可以去通过node去改写内容啊! 然后就有了现在的处理逻辑
但我预感,这不是最终的处理方式,我觉得还有更加便捷的,只不过我没有发现而已,当然这是以后更新的事情啦~
代码:
const TSConfig = require('../tsconfig.json')
// 第一步将ReactComponents以外的文件以默认格式转译
TSConfig.compilerOptions.module = 'none'
// 将首位替换为src
TSConfig.include.splice(0, 1, 'src')
// 排除ReactComponents下的所有文件
TSConfig.exclude.push('src/ReactComponents/**')
// 写入
fs.writeFileSync(path.resolve(__dirname, '../tsconfig.json'), JSON.stringify(TSConfig), 'utf-8')
// 运行
exec('tsc', () => {
console.log('tsc Success!');
nextTsc()
})
// 第二步 依旧tsc,只不过只转译 ReactComponents 文件夹内的文件,将格式改为 ES格式,用于下一步导出(因为react组件不参与分离,所以在这一步就得和其他方法在同一起跑线。详情看parsing.js逻辑)
const nextTsc = () => {
TSConfig.compilerOptions.module = 'ESNEXT'
TSConfig.include.splice(0, 1, 'src/ReactComponents')
TSConfig.exclude.pop()
fs.writeFileSync(path.resolve(__dirname, '../tsconfig.json'), JSON.stringify(TSConfig), 'utf-8')
exec('tsc', () => {
console.log('next tsc Success');
})
}
tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"module": "ESNEXT",
"target": "ESNEXT",
"lib": ["es5", "dom"],
"jsx": "react-jsx",
"moduleResolution": "node",
"rootDir": ".",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"noEmit": false
},
"exclude": ["node_modules", "jest"],
"include": ["src/ReactComponents", "typings"],
"types": ["commonjs"]
}
3. 命令文件重构
这次之所以想这么做,是因为之前去生成最后的lib太麻烦了,是在是受不了,找了找node的方法集合,果然被我找到了解决方式,其实上文就已经写出来了HAHA,没错儿就是exec
and execSync
!
有这个方法之后,我就讲执行步骤依次写到了一个文件内,届时要想生成新的lib直接去node 文件 就好了,多方便~
// start.js
const fs = require('fs')
const path = require('path')
const { exec, execSync } = require('child_process');
const { delFiles } = require('./controlFile')
const TSConfig = require('../tsconfig.json')
// 第一步将ReactComponents以外的文件以默认格式转译
TSConfig.compilerOptions.module = 'none'
TSConfig.include.splice(0, 1, 'src')
TSConfig.exclude.push('src/ReactComponents/**')
fs.writeFileSync(path.resolve(__dirname, '../tsconfig.json'), JSON.stringify(TSConfig), 'utf-8')
// tsx编译
exec('tsc', () => {
console.log('tsc Success!');
nextTsc()
})
// 第二步 依旧tsc,只不过只转译 ReactComponents 文件夹内的文件,将格式改为 ES格式,用于下一步导出(因为react组件不参与分离,所以在这一步就得和其他方法在同一起跑线。详情看parsing.js逻辑)
const nextTsc = () => {
TSConfig.compilerOptions.module = 'ESNEXT'
TSConfig.include.splice(0, 1, 'src/ReactComponents')
TSConfig.exclude.pop()
fs.writeFileSync(path.resolve(__dirname, '../tsconfig.json'), JSON.stringify(TSConfig), 'utf-8')
exec('tsc', () => {
console.log('next tsc Success');
nextStep()
})
}
// 接下来就按步骤走就好了
const nextStep = () => {
// 清空lib
delFiles(path.resolve(__dirname, '../lib'))
// 分离
execSync('node ./command/parsingJs.js')
console.log('parsing Success!');
// 转译
execSync('./node_modules/.bin/babel separate --copy-files -d lib')
console.log('babel Success!');
// 压缩
execSync('node ./command/uglify.js')
console.log('mifiles Success!');
// 删除之前遗留的无用文件夹
delFiles(path.resolve(__dirname, '../build'))
delFiles(path.resolve(__dirname, '../separate'))
console.log('delete Success!');
}
这里面你也看到了,我将关于文件操控的方法全部合并在了一个文件内,看着清净多了。
delFiles方法其实是getFiles的变种,用于删除目标文件夹及内部的所有文件,就不拿出来了。
4.ts转化
主要讲一个问题:
在将方法的函数转化为箭头函数格式(const a = () => {}) 后,进行 分离
步骤时,部分方法失效,报错为: xxx is not defined
.
很经典的报错了,我以为是我的方法写错了,检查过后并没有问题,那么问题就大了,这到底是什么引发的???
经过排查,锅还是得有箭头函数来背-。=
因为箭头函数在parsing内获取到的时候,是不包含const a =
这部分的,直接就是一个匿名函数,然后再直接进行拼接 就成了export default () => {}
这个样子,而那些出问题的函数,都是调用了自身的,也就是用了递归的方法,递归内部调用自己,结果自己是个匿名函数hhh,不报错都难
最后的解决方式就是将那些使用了递归的方法回归 function 声明,也就解决了。
分离(parsing.js)改的不多,主要就组件部分重新改了一下后缀和目标地址,其余的不变,就不再贴代码了
小结
这次本来是奔着ts来的,结果嘛... 命令文件废了蛮多的时间,ts部分大多都是死功夫,没什么含量,改的还算顺利吧,再加一些方法组件什么的,也不之后以后还更不更这个包,毕竟想要的功能都已经实现了.. 看以后有没有新点子咯~